Thumbprint logo

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

  • onChange
    required

    The 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 value prop passed in to this component.

    Type
    (value: string, event: React.ChangeEvent<HTMLInputElement>) => void
  • id

    Adds a HTML id attribute to the input. This is used for linking the HTML with an external Label, matching the v1 TextInput pattern. Note: when neither label nor accessibilityLabel is provided, react-aria logs a development-only console warning it cannot detect the native for/id association. The association is real and accessible; the warning can be ignored (or avoided by using the label prop).

    Type
    string
  • label

    Text that appears above the input, replacing the need for a separate Label component. 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. Use accessibilityLabel only when the design leaves no room for visible text.

    Type
    string
  • description

    Text that appears below the input, replacing the need for a separate FormNote component. When the input is in an error state with an errorMessage, the error message is shown in its place.

    Type
    React.ReactNode
  • errorMessage

    Error text that appears below the input, replacing the need for a separate FormNote component. Providing an errorMessage puts the input in the error state — no separate hasError needed.

    Type
    React.ReactNode
  • isDisabled

    Visually and functionally disable the input.

    Type
    boolean
    Default
    false
  • isReadOnly

    Adds readonly HTML attribute, allowing users to select (but not modify) the input.

    Type
    boolean
    Default
    false
  • isRequired

    Adds the required HTML attribute.

    Type
    boolean
    Default
    false
  • pattern

    A regular expression that the <input> element's value is checked against when submitting a form.

    Type
    string
  • maxLength

    The maximum number of characters that a user can enter. onChange will not fire if a user enters a character that exceeds maxLength.

    Type
    number
  • max

    The maximum value that can be entered. Valid when type=number.

    Type
    number
  • min

    The minimum value that can be entered. Valid when type=number.

    Type
    number
  • step

    The granularity of values that can be entered. Valid when type=number.

    Type
    number
  • hasError

    Makes the text and border color red.

    Type
    boolean
    Default
    false
  • placeholder

    Text that appears within the input when there is no value.

    Type
    string
  • size

    Controls the height and padding of the input.

    Type
    'small' | 'large'
    Default
    'large'
  • type

    Sets the type attribute on the input element.

    Type
    'email' | 'password' | 'text' | 'search' | 'tel' | 'number'
    Default
    'text'
  • inputMode

    A proposed specification that enables specification of virtual keyboard type in Chrome. Currently only supported in Chrome and Android.

    Type
    'numeric'
  • name

    The HTML name attribute 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.

    Type
    string
  • value

    The current value of the input.

    Type
    string | number
    Default
    ''
  • innerLeft

    Content that appears within the input on the left.

    Type
    React.ReactNode
  • innerRight

    Content that appears within the input on the right.

    Type
    React.ReactNode
  • onClick

    Function that fires when you click into the input.

    Type
    (event: React.MouseEvent<HTMLInputElement, MouseEvent>) => void
  • onFocus

    Fires when the input gains focus.

    Type
    (event: React.FocusEvent<HTMLInputElement>) => void
  • onBlur

    Fires when the input loses focus, regardless of whether the value has changed.

    Type
    (event: React.FocusEvent<HTMLInputElement>) => void
  • onKeyDown

    Fires when a key is pressed down with the input focused.

    Type
    (event: React.KeyboardEvent<HTMLInputElement>) => void
  • onKeyUp

    Fires when a key press is released with the input focused.

    Type
    (event: React.KeyboardEvent<HTMLInputElement>) => void
  • shouldFocusOnPageLoad

    This tells the browser to give the input focus when the page is loaded. This can only be used once on a page.

    Type
    boolean
    Default
    false
  • dataTestId

    A selector hook into the React component for use in automated testing environments. It is applied internally to the <input /> element.

    Type
    string
  • accessibilityLabel

    Accessible label for the input, applied as the aria-label attribute. Only needed if there is no label prop 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 for aria-label or aria-labelledby, this prop — or label — is the fix.

    Type
    string
  • autoComplete

    This tells the browser whether to attempt autocompletion of the input. Supports all values.

    Type
    React.InputHTMLAttributes<HTMLInputElement>['autoComplete']
  • enterKeyHint

    This tells the browser what action label (or icon) to present for the enter key on virtual keyboards. See MDN for more information.

    Type
    React.InputHTMLAttributes<HTMLInputElement>['enterKeyHint']
  • ref

    A ref to the underlying <input /> element.

    Type
    React.Ref<HTMLInputElement>

TextInputIconV2

Component that helps position icons within inputs.
  • children
    required

    An icon component from Thumbprint Icons. You should pair "Medium" icons with large inputs and "Small" icons with small inputs.

    Unlike the v1 TextInputIcon, there is no color prop: v2 semantic colors are theme-aware CSS variables, so the icon inherits the input's state color. To customize, style the icon element itself.

    Type
    React.ReactNode

TextInputClearButtonV2

Accessible button that makes it easy to add a "Clear" button to a text input. It should be used with the `innerRight` prop in `TextInputV2`. Its accessible name is derived from the input's `label` (or `accessibilityLabel`) — "Clear email address" rather than a bare "Clear input", so a screen reader user can tell several clear buttons on a page apart. It falls back to "Clear input" when the input has neither.
  • onClick
    required
    Type
    () => void