Text input
A labelled single-line field for short values such as names, addresses and IDs.
import { TextInput } from "@/ui";Usage
TextInput is controlled: pass value and update it in onChange. onChange receives the native change event, so read e.target.value.
const [name, setName] = useState("");
<TextInput
label="Campaign name"
placeholder="Q4 pipeline webinar"
value={name}
onChange={(e) => setName(e.target.value)}
/>The field fills the width of its container. Set the width on the parent, not on the input.
TextInput matches the Figma Text Input (Single-Line) and Form label components.
Always pass a placeholder. It defaults to the literal text "Placeholder". label has no default: leave it out and no label renders.
Sizes, icons and clearing
size="sm" is 38px tall with 12px gaps; the default md is 42px with 16px gaps. prefixIcon and suffixIcon take a Phosphor icon, drawn at 20px. clearable shows a 20px × once the field has a value.
<TextInput size="sm" placeholder="Search pages" prefixIcon={MagnifyingGlass} />
<TextInput placeholder="Search pages" prefixIcon={MagnifyingGlass} clearable value={q} onChange={(e) => setQ(e.target.value)} />
<TextInput label="Website" placeholder="yourcompany.com" suffixIcon={Globe} />Input types
type is passed straight to the <input>. Use email, url or password when the value is one of those, so browsers offer the right keyboard and autofill.
<TextInput type="email" label="Send the weekly report to" placeholder="revops@yourcompany.com" … />
<TextInput type="password" label="Page password" placeholder="Shared with the people who should see it" … />
<TextInput type="url" label="Page canonical URL" placeholder="https://" … />Validation
Set error to turn the border red, and errorMessage to say what to fix. The message only shows while error is true. Check the value on blur or on submit, not on every keystroke, so people are not told off while they are still typing.
const [gtm, setGtm] = useState("GTM-");
const [touched, setTouched] = useState(false);
const invalid = touched && !/^GTM-[A-Z0-9]{4,}$/.test(gtm);
<TextInput
label="Google Tag Manager ID"
placeholder="GTM-XXXXXXX"
value={gtm}
onChange={(e) => setGtm(e.target.value.toUpperCase())}
onBlur={() => setTouched(true)}
error={invalid}
errorMessage="Use the ID from your container, for example GTM-5KX2P7Q."
/>Write the message as the fix, not the fault: "Give the page an address", not "Invalid input".
States
<TextInput label="Default" placeholder="Landing page name" value="" />
<TextInput label="Filled" value="Demo request, EMEA" />
<TextInput label="Error" placeholder="demo-request" error errorMessage="Give the page an address." />
<TextInput label="Disabled" value="Forecast revenue you can defend" disabled />| Figma state | Border | Background | Text |
|---|---|---|---|
| Default | 1px --color-grey-200 | --color-white | Placeholder in --color-text-disabled |
| Selected (focus) | 1px --color-primary-500 | --color-white | |
| Filled (has a value) | 1px --color-primary-500 | --color-white | 14px --color-text-primary |
| Error | 1px --color-red | --color-white | Message 12px medium --color-red, 4px below |
| Disabled | none | --color-grey-50 | Label and value in --color-text-disabled |
The border stays blue once the field has a value, as Figma's Filled state shows, so people can tell filled fields from empty ones at a glance. The field is 10px by 16px padding (8px by 16px for sm), a 22px line, a 1px border and --radius-md (8px) corners. The label is 14px medium, 6px above the field.
There is no hint or helper text prop. For a hint, put a 12px --color-text-secondary paragraph under the field, as the page settings dialog does.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
label | ReactNode | Rendered in a <label> tied to the input. Left out, there is no label. | |
placeholder | string | "Placeholder" | Pass your own, or "" for none. |
value | string | Controlled value. | |
onChange | (event) => void | Receives the native change event. | |
type | string | "text" | Any HTML input type. |
error | boolean | false | Red border and aria-invalid. |
errorMessage | string | "" | Shown under the field only when error is true. |
disabled | boolean | false | |
size | "sm" | "md" | "md" | 38px or 42px tall. |
prefixIcon, suffixIcon | Phosphor icon | 20px, --color-grey-900. | |
clearable | boolean | false | × once filled. |
onClear | () => void | Defaults to calling onChange with an empty value. | |
className | string | Added to the wrapper <div>, not the input. Use for layout only. |
Any other prop (onBlur, onKeyDown, name, autoComplete, maxLength, aria-*) is passed to the <input>. In React 19 ref is an ordinary prop, so it reaches the <input> the same way; use it to focus the field, for example when a dialog opens.
Accessibility
- The
<label>is linked to the input with a generated id, so clicking the label focuses the field and screen readers read it as the field's name. - With
error, the input getsaria-invalid, and the message is linked witharia-describedbyand announced throughrole="alert". - Focus turns the border Primary 500, as Figma's Selected state shows. There is no extra ring.
- If the design has no visible label, leave out
labeland pass anaria-label. - The clear button is labelled "Clear" and puts focus back in the field.
- A required marker is not built in. If a field is required, say so in the label text ("Table name (required)") rather than with a red asterisk alone.
Do and don't
- Do write labels as a noun that names the value: "Page address", "Campaign name".
- Do use the placeholder for an example of the format, such as
GTM-XXXXXXXoryourcompany.com, not for instructions that disappear once someone types. - Don't use a TextInput for text longer than one line. Use Text area.
- Don't use one to pick from a known list. Use Dropdown or Radio.
- Don't restyle the field with
className; it lands on the wrapper and the input keeps its own styles.