Components
Text Input
Form inputs with sizes and style variations
TextInputV2 is built on React Aria and styled with Thumbprint v2 semantic tokens. Like v1 it is a controlled component: the visible text always matches the value prop, and onChange hands back the new value so you can store it in state.
The field also renders its own label and helper text. Prefer the label, description, and errorMessage props over composing Label and FormNote siblings: React Aria generates the ids and wires up the label association and aria-describedby, so the field is accessible with no ids to keep unique and no way for the error styling and the error text to fall out of sync.
Label
The label prop renders the label above the input and associates the two for assistive technologies. Every input needs a label; when the design has no room for a visible one, pass accessibilityLabel instead.
function TextInputExample() { const [value, setValue] = React.useState(''); return ( <TextInputV2 label="Email address" value={value} placeholder="example@example.com" onChange={setValue} /> ); }
Description
The description prop renders helper text below the input and points the input’s aria-describedby at it, so screen readers announce it along with the label.
function TextInputExample() { const [value, setValue] = React.useState(''); return ( <TextInputV2 label="Email address" description="We'll only use this to send you booking updates." value={value} placeholder="example@example.com" onChange={setValue} /> ); }
Error message
The errorMessage prop puts the input in the error state — there is no separate hasError to remember — and renders the message below the input. It replaces the description while the error is showing, so you can pass both and let the field swap them.
The error state is visual only, as it was in v1: it sets aria-invalid but leaves native validity alone, so a <form> holding an errored input still submits and your own validation stays in charge. isRequired is unaffected — it still applies the native required attribute, which the browser does enforce.
function TextInputExample() { const [value, setValue] = React.useState('example@example'); const errorMessage = value.includes('.') ? undefined : 'The email address you entered is incorrect.'; return ( <TextInputV2 label="Email address" description="We'll only use this to send you booking updates." errorMessage={errorMessage} value={value} onChange={setValue} /> ); }
Error styling without a message
hasError turns the label, text, and border red without rendering a note. Use it only when the message lives elsewhere — such as a summary at the top of the form — since an error state with no explanation leaves users stuck.
function TextInputExample() { const [value, setValue] = React.useState('example@example'); return ( <TextInputV2 label="Email address" hasError value={value} onChange={setValue} /> ); }
Sizes
Text inputs come in two sizes: small and large. large is the default. The heights are pinned to the same values as ButtonV2, so the two line up when placed side by side.
Small input
function TextInputExample() { const [value, setValue] = React.useState(''); return ( <TextInputV2 size="small" label="Email address" value={value} placeholder="example@example.com" onChange={setValue} /> ); }
Large input
function TextInputExample() { const [value, setValue] = React.useState(''); return ( <TextInputV2 size="large" label="Email address" value={value} placeholder="example@example.com" onChange={setValue} /> ); }
Disabled inputs
The isDisabled prop disables the input visually and functionally, and dims the built-in label along with it.
function TextInputExample() { const [value, setValue] = React.useState('example@example.com'); return ( <TextInputV2 label="Email address" isDisabled value={value} onChange={setValue} /> ); }
Read-only inputs
The isReadOnly prop adds the readonly attribute, letting users focus and select the text without editing it. There is no separate read-only look, and read-only inputs are still submitted with the form, so an errorMessage on one still shows. A disabled input is the opposite: it keeps the disabled treatment and its error is not shown, since the browser skips disabled fields when submitting.
function TextInputExample() { const [value, setValue] = React.useState('example@example.com'); return ( <TextInputV2 label="Email address" isReadOnly value={value} onChange={setValue} /> ); }
Icons and a clear button
innerLeft and innerRight render content inside the input. Wrap icons in TextInputIconV2 so they are spaced and aligned correctly, and use TextInputClearButtonV2 for a clear button — it hides itself while the input is empty. Clicking either one focuses the input.
Pair “Medium” icons with large inputs and “Small” icons with small inputs.
The clear button names itself after the input — “Clear street address” — taking the text from label, or accessibilityLabel when there is no visible label. That way a screen reader user can tell several clear buttons on a page apart. With neither prop it falls back to “Clear input”.
function ClearableTextInput() { const [value, setValue] = React.useState('1355 Market St.'); return ( <TextInputV2 label="Street address" value={value} placeholder="Enter an address" innerLeft={ <TextInputIconV2> <ContentModifierMapPinMedium /> </TextInputIconV2> } innerRight={<TextInputClearButtonV2 onClick={() => setValue('')} />} onChange={setValue} /> ); }
Using an external Label and FormNote
For call sites that have not moved to the props yet, a sibling Label and FormNote still work: the id prop lands on the <input> element, so Label’s for associates the two natively. You are then responsible for keeping the ids unique and, in the error state, for keeping the hasError flags on all three components in sync — which is why the label, description, and errorMessage props are the better choice for new code.
In development, React Aria logs a console warning for this pattern because it can’t detect the external for/id association. The association is real and accessible, so the warning can be ignored; passing label or accessibilityLabel silences it.
function TextInputExample() { const [value, setValue] = React.useState(''); return ( <div> <Label for="example-text-input-v2">Email address</Label> <TextInputV2 id="example-text-input-v2" value={value} placeholder="example@example.com" onChange={setValue} /> <div className="mt1"> <FormNote>We'll only use this to send you booking updates.</FormNote> </div> </div> ); }
Props
TextInputV2
onChangerequiredThe function that is called when the input value changes.
It receives two arguments:
onChange(newValue, event).The consumer of this component should use that data to update the
valueprop passed in to this component.Type(value: string, event: React.ChangeEvent<HTMLInputElement>) => voididAdds a HTML
idattribute to the input. This is used for linking the HTML with an external Label, matching the v1TextInputpattern. Note: when neitherlabelnoraccessibilityLabelis provided, react-aria logs a development-only console warning it cannot detect the nativefor/idassociation. The association is real and accessible; the warning can be ignored (or avoided by using thelabelprop).TypestringlabelText that appears above the input, replacing the need for a separate
Labelcomponent. The label is automatically associated with the input for assistive technologies.Prefer this over
accessibilityLabel: a visible label serves everyone, not only screen reader users, and clicking it focuses the input. UseaccessibilityLabelonly when the design leaves no room for visible text.TypestringdescriptionText that appears below the input, replacing the need for a separate
FormNotecomponent. When the input is in an error state with anerrorMessage, the error message is shown in its place.TypeReact.ReactNodeerrorMessageError text that appears below the input, replacing the need for a separate
FormNotecomponent. Providing anerrorMessageputs the input in the error state — no separatehasErrorneeded.TypeReact.ReactNodeisDisabledVisually and functionally disable the input.
TypebooleanDefaultfalseisReadOnlyAdds
readonlyHTML attribute, allowing users to select (but not modify) the input.TypebooleanDefaultfalseisRequiredAdds the
requiredHTML attribute.TypebooleanDefaultfalsepatternA regular expression that the
<input>element's value is checked against when submitting a form.TypestringmaxLengthThe maximum number of characters that a user can enter.
onChangewill not fire if a user enters a character that exceedsmaxLength.TypenumbermaxThe maximum value that can be entered. Valid when
type=number.TypenumberminThe minimum value that can be entered. Valid when
type=number.TypenumberstepThe granularity of values that can be entered. Valid when
type=number.TypenumberhasErrorMakes the text and border color red.
TypebooleanDefaultfalseplaceholderText that appears within the input when there is no
value.TypestringsizeControls the height and padding of the input.
Type'small' | 'large'Default'large'typeSets the
typeattribute on the input element.Type'email' | 'password' | 'text' | 'search' | 'tel' | 'number'Default'text'inputModeA proposed specification that enables specification of virtual keyboard type in Chrome. Currently only supported in Chrome and Android.
Type'numeric'nameThe HTML
nameattribute that will be passed to the input. It is required if working with a form that uses<form action="" method="">to submit data to a server.TypestringvalueThe current value of the input.
Typestring | numberDefault''innerLeftContent that appears within the input on the left.
TypeReact.ReactNodeinnerRightContent that appears within the input on the right.
TypeReact.ReactNodeonClickFunction that fires when you click into the input.
Type(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => voidonFocusFires when the input gains focus.
Type(event: React.FocusEvent<HTMLInputElement>) => voidonBlurFires when the input loses focus, regardless of whether the value has changed.
Type(event: React.FocusEvent<HTMLInputElement>) => voidonKeyDownFires when a key is pressed down with the input focused.
Type(event: React.KeyboardEvent<HTMLInputElement>) => voidonKeyUpFires when a key press is released with the input focused.
Type(event: React.KeyboardEvent<HTMLInputElement>) => voidshouldFocusOnPageLoadThis tells the browser to give the input focus when the page is loaded. This can only be used once on a page.
TypebooleanDefaultfalsedataTestIdA selector hook into the React component for use in automated testing environments. It is applied internally to the
<input />element.TypestringaccessibilityLabelAccessible label for the input, applied as the
aria-labelattribute. Only needed if there is nolabelprop or associated label element, or if the input needs additional context (e.g., for inputs with only icons). If you see react-aria's development-only warning asking foraria-labeloraria-labelledby, this prop — orlabel— is the fix.TypestringautoCompleteThis tells the browser whether to attempt autocompletion of the input. Supports all values.
TypeReact.InputHTMLAttributes<HTMLInputElement>['autoComplete']enterKeyHintThis tells the browser what action label (or icon) to present for the enter key on virtual keyboards. See MDN for more information.
TypeReact.InputHTMLAttributes<HTMLInputElement>['enterKeyHint']refA ref to the underlying
<input />element.TypeReact.Ref<HTMLInputElement>
TextInputIconV2
childrenrequiredAn icon component from Thumbprint Icons. You should pair "Medium" icons with
largeinputs and "Small" icons withsmallinputs.Unlike the v1
TextInputIcon, there is nocolorprop: v2 semantic colors are theme-aware CSS variables, so the icon inherits the input's state color. To customize, style the icon element itself.TypeReact.ReactNode
TextInputClearButtonV2
onClickrequiredType() => void