Skip to content

feat: TextField component#4904

Open
wonderlul wants to merge 1 commit intocallstack:@adrcotfas/feat/dynamic_themefrom
wonderlul:feat/TextField
Open

feat: TextField component#4904
wonderlul wants to merge 1 commit intocallstack:@adrcotfas/feat/dynamic_themefrom
wonderlul:feat/TextField

Conversation

@wonderlul
Copy link
Copy Markdown
Collaborator

Motivation

File structure
The component is split by variant (filled/ and outlined/) and a root that wires shared behavior. Each area keeps logic, styles, utils, and constants in separate files. That follows patterns already used elsewhere in the library, but goes one step further so responsibilities stay obvious: variant-specific layout and theming do not drown in shared code, and the public API file stays focused on behavior and types.

LeftAccessory / RightAccessory vs TextInput adornments
TextInput composes leading and trailing content through left and right, which are built around icons and affixes (TextInput.Icon, TextInput.Affix) and internal adornment types. TextField instead exposes LeftAccessory and RightAccessory as render props (component types). The field passes curated layout and state—notably the merged style for alignment with the field, plus status, multiline, and editable—so accessories stay aligned with the input without re-implementing field internals. That supports arbitrary leading/trailing UI (clear actions, custom buttons, non-icon content) while still inheriting the important structural styles from the component.

filled / outlined instead of flat / outlined
Material Design 3 describes text fields in terms of filled and outlined styles. The existing TextInput API uses mode: 'flat' | 'outlined', where “flat” corresponds to the filled look. The new component names variants filled and outlined so the public API matches MD3 language and is easier to map from the spec and design tools.

Style overrides
TextField is built as a small stack of clear layers, and each layer can be adjusted without fighting the rest. The outer pressable wrapper, the field shell (border, background, row that includes accessories), and the inner content wrapper (label + TextInput) each accept dedicated style props (pressableStyle, fieldStyle, containerStyle). The underlying TextInput still uses the normal style prop (and the rest of TextInputProps) for typography, padding, and input-specific layout. Label and helper text can be customized through labelProps and helperProps (including style). Leading/trailing UI uses LeftAccessory / RightAccessory, which receive a prepared style from the field so custom content stays aligned while remaining fully under your control. Together, this gives predictable “override the part you mean” behavior instead of a single opaque style that’s hard to reason about.

TextField instead of TextInput
Material Design 3 uses the term text field for this control. Exporting a second “Paper” input named TextInput would also blur the built-in React Native TextInput in imports and documentation. A dedicated TextField name keeps the design-system component clearly namespaced and aligned with MD3, while the underlying control remains React Native’s TextInput where appropriate.

Positioning relative to TextInput
TextField is intended as the modern replacement path for form text entry in react-native-paper: implementation is structured for clarity and maintainability, and it adopts MD3-oriented theming (including use of the PlatformColor API where it fits platform tokens). Compared to the legacy TextInput stack, this design aims to be easier to follow, less ad hoc in how variants and layout are split, and more efficient in how styles and state are applied—giving teams a refreshed, spec-aligned building block for new work without forcing an immediate break for existing TextInput users.

Order of merging

#4901

Related issue

#4878
#4329
#4235

Test plan

Run the Example app.

filled
text-field-filled

outlined
text-field-outlined

@callstack-bot
Copy link
Copy Markdown

callstack-bot commented Apr 22, 2026

Hey @wonderlul, thank you for your pull request 🤗. The documentation from this branch can be viewed here.

Copy link
Copy Markdown
Collaborator

@adrcotfas adrcotfas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Below are some components described in the specs I see were not covered. Do we want them?

  • in all error examples from the specs there's a trailing error icon
  • the option to have leading and trailing text (called prefix and suffix) for currency, units of measurement etc; for suffix, the text field is aligned to the end instead of start; - character counter (aligned to the end, same style as supporting text)
  • The native filled TextField has an animation for the outline when selecting it: center to edges reveal
  • see https://m3.material.io/components/text-fields/guidelines

Some other notes:

  • Please double check the tokens from the specs. I added some comments about opacity/color for certain elements that was missing; maybe there are other inconsistencies.
  • A more complex example would be nice to cover more use cases and these elements, if we add them.

Here's a recording from this app showing the features mentioned above: https://github.com/material-components/material-components-android. I recommend installing this app to double check how TextField looks and behaves like in native.

screen-20260423-165915-1776952725646.mp4

borderColor: hasActiveOutline ? activeOutlineColor : inactiveOutlineColor,
},
disabled && $disabledStyle,
$fieldStyleOverride,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional? It's already part of fieldStyles above.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is intentional. outlineStyles are kind of additional outline to handle the focus state that has to be overridden as well.

paddingHorizontal: TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL,
fontSize: HELPER_FONT_SIZE,
fontWeight: '400',
textAlign: 'left',
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should consider RTL instead of hardcoding left.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering RTL using left is actually handling both cases depending on the settings. Do you have something else in mind?

Copy link
Copy Markdown
Collaborator

@adrcotfas adrcotfas Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By considering RTL you mean outlined/logic.ts writingDirection? Doesn't the text still anchor to the left side regardless of writingDirection?
Shouldn't this be auto instead of left or just remove it?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With auto it aligns text to the left and doesn't respect the RTL settings. I guess with textAlign: left it considers left as the start no matter if it's left or right :-). Don't even ask me how long it took me to figure that out.

@@ -0,0 +1,20 @@
import {
ACCESORY_SIZE,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo for ACCESSORY_SIZE here and in styles.ts:, filled/constants.ts, outlined/constants.ts.

@@ -0,0 +1,167 @@
import { Animated, StyleProp, TextStyle, ViewStyle } from 'react-native';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filled variant does not handle RTL as the outlined variant does. Also, some RTL tests would be useful.


let inactiveOutlineColor = colors.outline;

if (disabled) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to specs:

Outlined text field disabled outline color -> md.sys.color.on-surface
Outlined text field disabled outline opacity -> 0.12

{
backgroundColor: disabled ? surfaceContainerHighest : surfaceVariant,
},
disabled && $disabledStyle,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The outlined variant applies disabled && $disabledStyle per element (input, label, accessories). The filled variant applies it only to the field parent.

$animatedLabelTextStyles has no disabled dimming.

According to specs:

Filled text field disabled container opacity -> 0.04

autoCapitalize="none"
pressableStyle={styles.field}
/>
<TextField
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No outline is visible here.


import {
ACCESORY_SIZE,
HELPER_FONT_SIZE,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with the specs, I would rename all occurrences of helper to supportingText and accessory to icon.

@@ -0,0 +1,167 @@
import { Animated, StyleProp, TextStyle, ViewStyle } from 'react-native';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the specs placeholders are hidden until the TextField is selected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants