close
Skip to content

feat: TextField component#4909

Open
wonderlul wants to merge 18 commits intocallstack:v6from
wonderlul:feat/TextField-v6
Open

feat: TextField component#4909
wonderlul wants to merge 18 commits intocallstack:v6from
wonderlul:feat/TextField-v6

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 30, 2026

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

@wonderlul wonderlul force-pushed the feat/TextField-v6 branch from 146de2a to a1a20fd Compare May 4, 2026 10:27
Comment thread example/src/Examples/TextFieldExample.tsx Outdated
Comment thread src/components/TextField/TextField.tsx
Comment thread src/components/TextField/TextFieldIcon.tsx
Comment thread src/components/TextField/filled/logic.ts Outdated
Comment thread src/components/TextField/utils.ts Outdated
Comment thread example/src/Examples/TextFieldExample.tsx
Comment thread example/src/Examples/TextFieldExample.tsx Outdated
supportingTextProps?.style,
];

const $inputStyles: StyleProp<TextStyle> = [
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.

At least on Android there's a visual bug with the opacity of the input text.
When disabled, the opacity on the input text appears more than it should be (near invisible).
The fix I found was to extract a $inputWrapperStyles and move the { flex: 1 }, disabled && $disabledStyle to it then place the RN TextInput from TextField.tsx inside of a <View style={$inputWrapperStyles}>.

Comment thread src/components/TextField/filled/logic.ts Outdated
Comment thread src/components/TextField/outlined/logic.ts
@adrcotfas
Copy link
Copy Markdown
Collaborator

Here's a comment from my friend Claude about duplicate code:

logic.ts -- highest duplication (~28%)

The most significant overlap. Both files share these sections verbatim:

Block Lines each
Props destructuring ~13
isRTL extraction 1
getLabelColor(...) call ~5
getSupportingTextColor(...) call ~5
$animatedLabelTextStyles array ~9
$containerStyles array ~4
$supportingTextStyles array ~8
$counterStyles array ~8
$prefixStyles array ~6
$suffixStyles array ~6
$leadingAccessoryStyles ~3
$trailingAccessoryStyles ~3
Total ~71 identical lines

Outlined has 233 lines, filled has 280 -- so ~71/233 (30%) and ~71/280 (25%) respectively are shared logic.

What's genuinely variant: the $fieldStyles (filled adds backgroundColor), $outlineStyles (border vs bottom-bar), $animatedActiveOutlineStyles (filled-only), $disabledBackgroundStyles (undefined in outlined, overlay in filled), and the labelBackgroundColor extraction
(outlined only).


styles.ts -- low duplication (~10%)

$labelTextStyle is byte-for-byte identical in both files (5 lines). It belongs in the shared ../styles.ts.

Everything else differs meaningfully:

  • $fieldStyle: same keys, borderRadius (outlined) vs borderTopStart/EndRadius (filled)
  • $outlineStyle: full-perimeter absolute (outlined) vs bottom-only absolute (filled)
  • $containerStyle: alignItems: 'center' (outlined) vs 'flex-end' (filled)
  • $labelWrapperStyle: outlined adds paddingHorizontal, filled is empty
  • $disabledBackgroundStyle: filled-only

constants.ts -- minimal duplication (~5%)

LABEL_START_OFFSET_WITHOUT_ACCESSORY resolves to TEXT_FIELD_INPUT_WRAPPER_PADDING_HORIZONTAL in both -- could live in shared ../constants.ts.

Everything else is variant: LABEL_PADDING_HORIZONTAL and the RTL translate constants are outlined-only; MULTILINE_PADDING_TOP is filled-only; ACTIVE_LABEL_TOP_POSITION uses different formulas; the opacity values differ (0.12 vs 0.04).


utils.ts -- 0% unifiable

Both export getOutlineColor but with incompatible signatures: outlined takes hasError: boolean, filled takes status?: 'error' | 'disabled'. Same name, different contract -- unifying them would require a signature change that ripples into both logic.ts files.


Overall

File Outlined lines Filled lines Duplicated %
logic.ts 233 280 ~71 ~28%
styles.ts 45 54 ~5 ~10%
constants.ts 49 35 ~2 ~5%
utils.ts 39 34 0 0%
Total 366 403 ~78 ~20%

@adrcotfas
Copy link
Copy Markdown
Collaborator

adrcotfas commented May 7, 2026

Some more issues I found:

  • Outlined + Multiline in combination with any leading icon/ trailing icon/ error/suffix/ prefix has a bug; give it a try as now it's much easier to manually test combinations
  • We should not allow prefix + suffix in the same field

Edit: the filled version lost the top rounded corners when in error mode and after toggling the "disabled" state.

Comment thread src/components/TextField/TextFieldIcon.tsx
@adrcotfas
Copy link
Copy Markdown
Collaborator

When the helper text is empty, the counter gets aligned to start. It should stay aligned to end.

@adrcotfas
Copy link
Copy Markdown
Collaborator

Error seems to take precedence over Disabled but I would expect that a disabled field is not editable even if it has "error".

@wonderlul
Copy link
Copy Markdown
Collaborator Author

Some more issues I found:

  • The filled version lost the top rounded corners Or not - it was a weird visual bug occuring once
  • Outlined + Multiline in combination with any leading icon/ trailing icon/ error/suffix/ prefix has a bug; give it a try as now it's much easier to manually test combinations
  • We should not allow prefix + suffix in the same field

Multi-line fixed.

I disagree with disallowing prefix + suffix. I think it's up to the developer to decide whether to use both, one or neither, but we shouldn't disable the option for developers.

@wonderlul
Copy link
Copy Markdown
Collaborator Author

Error seems to take precedence over Disabled but I would expect that a disabled field is not editable even if it has "error".

Yeah, this is on purpose — we’re using a clear order for those states.

My thinking was: if there’s an error, the field shouldn’t really be blocked. You usually need to change the value to fix it, so disabling it felt backwards.

If we’re okay with that idea, we can stick to this gradation and it keeps the code a lot simpler — less special cases.

I know some developers expect disabled to always win even when error is set; if that’s what we want product-wise, we can always revisit it. Let me know.

@wonderlul wonderlul requested a review from adrcotfas May 7, 2026 12:35
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