Skip to content

feat: TextInput component#4909

Merged
satya164 merged 34 commits into
callstack:mainfrom
wonderlul:feat/TextField-v6
Jun 3, 2026
Merged

feat: TextInput component#4909
satya164 merged 34 commits into
callstack:mainfrom
wonderlul:feat/TextField-v6

Conversation

@wonderlul
Copy link
Copy Markdown
Collaborator

@wonderlul wonderlul commented Apr 30, 2026

BREAKING CHANGE: TextInput has been rewritten for Material Design 3. The public API, adornments, variant naming, and styling overrides have changed. See the migration section below for required updates.

Summary

Replaces the Paper 5.x TextInput with a new MD3-aligned implementation. The component name stays TextInput, but most props and composition patterns have changed.

The new TextInput supports filled and outlined variants, floating labels, supportingText, a built-in character counter, prefix / suffix, leading/trailing startAccessory / endAccessory render props, a custom render prop for masked inputs, and improved accessibility. Colors and layout follow the theme; per-prop color overrides for outlines and underlines were removed.

What changed

Before (v5) After (v6)
mode="flat" variant="filled" (default)
mode="outlined" variant="outlined"
left / right element props startAccessory / endAccessory render props
TextInput.Affix prefix / suffix strings, or custom accessories
HelperText below the field supportingText on the field
label as string or ReactNode label as string only
dense, contentStyle, outlineStyle, underlineStyle removed — use style and theme
textColor, underlineColor, activeUnderlineColor, outlineColor, activeOutlineColor removed — use style and theme
disabled blocked interaction disabled blocks interaction; use editable={false} for read-only fields that can still be focused

Still supported

  • TextInput.Icon — use inside startAccessory / endAccessory
  • render — swap the native input (e.g. masked inputs)
  • selectionColor and cursorColor — inherited from React Native TextInput props
  • ref — exposes TextInputHandles (focus, blur, clear, isFocused, setNativeProps, setSelection); clear() also syncs internal state and label animation

What was removed

  • TextInput.Affix
  • HelperText as the recommended companion for text fields (use supportingText instead)
  • mode, left, right
  • dense, contentStyle, outlineStyle, underlineStyle
  • textColor, underlineColor, activeUnderlineColor, outlineColor, activeOutlineColor

Migration

Import stays the same:

import { TextInput, type TextInputProps } from 'react-native-paper';

Variant

// Before (v5)
<TextInput mode="flat" label="Email" />
<TextInput mode="outlined" label="Password" />

// After (v6)
<TextInput variant="filled" label="Email" />
<TextInput variant="outlined" label="Password" />

Icons and adornments

left / right element props become startAccessory / endAccessory render functions. Spread the props TextInput passes in — they include layout style, plus error, disabled, and multiline.

// Before (v5)
<TextInput
  label="Search"
  value={text}
  onChangeText={setText}
  left={<TextInput.Icon icon="magnify" />}
  right={<TextInput.Affix text={`${text.length}/80`} />}
/>

// After (v6)
<TextInput
  label="Search"
  value={text}
  onChangeText={setText}
  startAccessory={(props) => <TextInput.Icon {...props} icon="magnify" />}
  endAccessory={(props) => (
    <TextInput.Icon {...props} icon="close" onPress={() => setText('')} />
  )}
/>

Replace TextInput.Affix with prefix / suffix for inline text, or a custom endAccessory:

// Before (v5) — affix as trailing adornment
<TextInput
  right={<TextInput.Affix text="/100" />}
  maxLength={100}
/>

// After (v6) — prefix/suffix and/or counter
<TextInput
  prefix="$"
  suffix="/100"
  maxLength={100}
  counter
/>

Note: prefix and suffix are shown only when the label is floated (field focused or has a value).

Supporting text and errors

// Before (v5)
<>
  <TextInput label="Email" error={hasError} />
  <HelperText type="error" visible={hasError}>
    Enter a valid email
  </HelperText>
</>

// After (v6)
<TextInput
  label="Email"
  error={hasError}
  supportingText={hasError ? 'Enter a valid email' : 'We will never share your email'}
/>

When error is true and no endAccessory is provided, a default error icon is shown on the trailing side.

Disabled vs read-only

// Disabled — not editable, not focusable, disabled styling
<TextInput disabled label="Email" value={email} />

// Read-only — can focus and select text, but not edit
<TextInput editable={false} label="Email" value={email} />

Styling and colors

Outline, underline, label, and disabled colors now come from the theme. Override input text with the standard style prop; override accent colors via theme or, where applicable, inherited props:

// Before (v5)
<TextInput
  dense
  contentStyle={{ paddingHorizontal: 12 }}
  outlineStyle={{ borderRadius: 12 }}
  outlineColor="#79747E"
  activeOutlineColor="#6750A4"
  textColor="#1C1B1F"
  style={{ fontSize: 16 }}
/>

// After (v6)
<TextInput
  theme={{
    ...MD3LightTheme,
    colors: {
      ...MD3LightTheme.colors,
      outline: '#79747E',
      primary: '#6750A4',
    },
  }}
  style={{ fontSize: 16, color: '#1C1B1F' }}
  selectionColor="#6750A4"
  cursorColor="#6750A4"
/>

Custom input (render prop)

<TextInput
  label="Phone"
  render={(props) => <TextInputMask {...props} mask="+[00] [000] [000] [000]" />}
/>

Imperative handle

const ref = React.useRef<TextInputHandles>(null);

<TextInput ref={ref} label="Name" defaultValue="Jane" onChangeText={setName} />;

ref.current?.clear(); // clears text, syncs counter/label animation, calls onChangeText('')

New props reference

Prop Description
variant 'filled' (default) or 'outlined'
supportingText Helper/error text below the field
counter Shows currentLength/maxLength; requires maxLength
prefix / suffix Short inline strings inside the field
startAccessory / endAccessory Render props for leading/trailing content
disabled Disables the field (separate from editable={false})
render Custom native input renderer

Related

Test plan

Run the Example app and verify:

  • filled variant — label animation, underline indicator, disabled overlay, error state
  • outlined variant — outline, label notch behavior, error state
  • startAccessory / endAccessory with TextInput.Icon and custom content
  • prefix / suffix visibility when focused vs blurred
  • counter with maxLength
  • supportingText with and without error
  • disabled vs editable={false}
  • render prop with a custom input
  • ref.clear() on controlled and uncontrolled fields
  • RTL layout (accessories swap sides)
  • Multiline fields

Screenshots

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 Outdated
Comment thread src/components/TextField/TextFieldIcon.tsx Outdated
Comment thread src/components/TextField/filled/logic.ts Outdated
Comment thread src/components/TextField/utils.ts Outdated
Comment thread example/src/Examples/TextFieldExample.tsx Outdated
Comment thread example/src/Examples/TextFieldExample.tsx Outdated
Comment thread src/components/TextField/filled/logic.ts Outdated
Comment thread src/components/TextField/filled/logic.ts Outdated
Comment thread src/components/TextField/outlined/logic.ts Outdated
@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/TextInput/TextInputIcon.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
Copy link
Copy Markdown
Member

@satya164 satya164 left a comment

Choose a reason for hiding this comment

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

Let's keep the code style consistent with other parts of the codebase.

Avoid too many small helpers. The code is hard for me to follow because it's split between so many parts. Prefer normal hooks and components for composition instead of helper functions.

We're not using $ prefix for styles. Use StyleSheet.create API so it uses atomic styles on React Native Web. Avoid unnecessary inline styles.

Also, avoid comments like these:

// ============
// OPACITY
// ============

Use normal multiline or single-line comments when necessary and don't comment on the obvious.

@wonderlul
Copy link
Copy Markdown
Collaborator Author

Let's keep the code style consistent with other parts of the codebase.

Avoid too many small helpers. The code is hard for me to follow because it's split between so many parts. Prefer normal hooks and components for composition instead of helper functions.

We're not using $ prefix for styles. Use StyleSheet.create API so it uses atomic styles on React Native Web. Avoid unnecessary inline styles.

Also, avoid comments like these:

// ============
// OPACITY
// ============

Use normal multiline or single-line comments when necessary and don't comment on the obvious.

In the context of hooks vs. helpers, I see it more as a matter of convention and preference. If reusable logic does not manage application state, I usually prefer helper/util terminology over hooks. The helpers I created contain reusable logic, which is why I decided to extract them. Similarly, when a piece of logic becomes relatively large (e.g. getFilledTextFieldData), I tend to move it into a helper as well.

I split the logic into separate places because, from my perspective, it improves readability through clearer separation of responsibilities. Of course, this can be somewhat subjective. I think the current solution is a reasonable middle ground. I removed the filled and outlined folders and moved their logic into individual files within the main folder. Considering the complexity of the component, the current file structure feels like a good balance between readability and organization.

I also added the StyleSheet API for static styles, although most of the styles in this component are dynamic. I removed the $ notation as well.

As for comments, I only added them where I felt additional context improved readability — mainly around more complex functions, variables, styles, or to separate logical sections within files. Following your suggestion, I updated them to regular multiline or single-line comments.

@wonderlul wonderlul requested a review from satya164 May 14, 2026 08:55
@wonderlul wonderlul force-pushed the feat/TextField-v6 branch 4 times, most recently from bf4638d to 73642ae Compare May 14, 2026 10:59
@wonderlul wonderlul force-pushed the feat/TextField-v6 branch from e3d5f56 to daea379 Compare May 14, 2026 11:17
@adrcotfas adrcotfas added the v6 label May 14, 2026
Copy link
Copy Markdown
Member

@satya164 satya164 left a comment

Choose a reason for hiding this comment

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

Thanks for the PR. I left few comments.

And found few issues:

  • Are inputs supposed to look this big on the web?
  • Alignment seems off for outlined input on the web
    Image
  • Tapping on leading icon in the input seems to unfocus in the input. Since it looks like part of the input, I'd expect it to act like tapping on the input. Ideally it should be positioned absolutely on top and text input should have padding to offset it
  • Measurements seems a bit off on iOS (the app doesn't build on Android, so haven't checked). The placeholder text seems larger and as a result has less space below it compared to the official example.
    Image
  • The input supports multiline but leading and trailing icons stay vertically centered. I'd expect them to be aligned at the top when there is multiline text.

Also I think while we should make breaking changes to align with MD3, it'd be valuable to keep the API same where we don't need to deviate.

  • TextField matches MD3 naming, but I think TextInput is better here since users already know it, and it'll be easier migration. So we should keep TextInput as the component name.
  • Missing props: textColor, underlineColor, activeUnderlineColor, outlineColor, activeOutlineColor, selectionColor, cursorColor - we should at leasy support selection and cursor color.
  • Missing render prop to render a custom input. This is necessary to use an input implementation that supports masking etc.

Comment thread example/src/Examples/TextFieldExample.tsx Outdated
Comment thread src/components/TextField/hooks.ts Outdated
Comment thread src/components/TextField/hooks.ts Outdated
Comment thread src/components/TextField/hooks.ts Outdated
Comment thread src/components/TextField/hooks.ts Outdated
Comment thread src/components/TextField/TextField.tsx Outdated
Comment thread src/components/TextField/TextField.tsx Outdated
Comment thread src/components/TextField/TextField.tsx Outdated
Comment thread src/components/TextField/TextField.tsx Outdated
Comment thread src/components/TextField/TextField.tsx Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new MD3-aligned TextField component to react-native-paper, alongside docs, example app integration, and a comprehensive test suite.

Changes:

  • Added TextField component implementation (filled/outlined variants), including shared layout utils, animation/state hooks, constants, and styles.
  • Exposed TextField (and related types) from the library entrypoint and added TextField.Icon accessory helper component.
  • Added Example app screen plus documentation wiring (screenshots + docs config) and extensive snapshots/tests.

Reviewed changes

Copilot reviewed 16 out of 19 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/index.tsx Exports TextField and its public types from the library entrypoint.
src/components/TextField/utils.ts Implements shared/variant style computation, colors, animation style objects, and accessibility helpers.
src/components/TextField/TextFieldIcon.tsx Adds TextField.Icon helper component and exports accessory/icon prop types.
src/components/TextField/TextFieldErrorIcon.tsx Adds default error indicator shown when error is true and no trailing accessory is provided.
src/components/TextField/TextField.tsx Core TextField component: layout composition, label, accessories, supporting text, counter, render prop plumbing.
src/components/TextField/styles.ts Shared + variant styles for field layout, accessories, label wrapper, outline, etc.
src/components/TextField/index.ts Creates compounded export (TextField with .Icon).
src/components/TextField/hooks.ts Implements state management for value/focus/flags, memoized layout derivation, and prop merging for render.
src/components/TextField/constants.ts MD3 token-based constants for sizing, paddings, animation duration, label positions, etc.
src/components/tests/TextField.test.tsx Adds a large test suite covering rendering, accessories, a11y props, counters, RTL, focus behavior, etc.
src/components/tests/snapshots/TextField.test.tsx.snap Snapshots for the new TextField test suite.
jest/testSetup.js Minor formatting change in mocked icon implementation (test infra).
example/src/Examples/TextFieldExample.tsx Adds an interactive Example app screen for TextField variants and prop toggles.
example/src/ExampleList.tsx Registers the new TextField example in the Example app list.
docs/src/data/screenshots.js Adds TextField screenshots (filled/outlined) for docs rendering.
docs/src/components/PropTable.tsx Adds type-link mappings for new TextField render prop types and adjusts formatting.
docs/docusaurus.config.js Registers TextField and TextFieldIcon in the docs component pages/config and edit URL mapping.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/TextField/hooks.ts Outdated
Comment thread src/components/TextField/TextField.tsx Outdated
Comment thread src/components/TextField/TextField.tsx Outdated
@wonderlul wonderlul changed the title feat: TextField component feat: TextInput component May 27, 2026
@wonderlul
Copy link
Copy Markdown
Collaborator Author

Either we allow user to override styles or we don't

I don't follow this line of thought. We allow users to override what we deem safe and not an implementation detail. We don't allow users to override implementation details that can change just because of a refactor. Things like selection color, cursor color aren't going to change because of a refactor. It's a text input - these things will never become invalid no matter how you write this component.

I agree. That's what I meant by Text color can be update with styles prop. selectionColor and cursorColor are acceptable props for TextField since it extends TextInputProps so they can override the defaults. Other colors can be overridden with custom theming. Apart from the inherited ones, I wouldn’t choose more or less important styles, because that’s relative. One developer may want to override the border on an input, while another may want to change the opacity for the disabled variant. Nevertheless, if you want to do it, I’m open to making changes. I’d just ask you to point out specifically which ones you’d like to make overridable.

@wonderlul
Copy link
Copy Markdown
Collaborator Author

@satya164 I’d also ask you to take a look at the newly added 12-migration.md document. I’m not sure whether we want to present it as a separate document or somehow merge/edit the Getting Started guide instead. Let me know how you’d like to approach it.

@wonderlul wonderlul requested a review from satya164 May 28, 2026 06:47
Comment thread src/components/TextInput/hooks.ts Outdated
Comment thread src/components/TextInput/hooks.ts Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 39 out of 43 changed files in this pull request and generated 4 comments.

Comment thread src/components/TextInput/TextInput.tsx
Comment thread src/components/TextInput/hooks.ts
Comment thread src/components/TextInput/hooks.ts Outdated
Comment thread src/components/TextInput/hooks.ts
@wonderlul wonderlul requested a review from satya164 June 3, 2026 03:10
@satya164 satya164 requested a review from Copilot June 3, 2026 08:46
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 39 out of 49 changed files in this pull request and generated 3 comments.

Comment thread src/components/TextInput/TextInputIcon.tsx
Comment thread src/components/TextInput/hooks.ts
Comment thread src/components/TextInput/TextInput.tsx
@satya164 satya164 merged commit 8b2b1eb into callstack:main Jun 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Outlined TextInput label background visible through Modal backdrop, transparent background causes outline strikethrough

5 participants