A single-line text input with a label, help text and validation. Supports icon and text adornments, every native input type, native and custom validation, server errors and form submission.
TextField renders the label, the input, the description and the error message, and wires them together for assistive technology. Pass value and onChange to control it, or defaultValue to leave it uncontrolled.
onChange receives a string
React Aria calls onChange with the new value, not a DOM event, so you can
pass a state setter directly: onChange={setName}. Use onInput or
onKeyDown if you need the raw event.
outline for most forms, filled for dense settings pages and tinted surfaces, underlined for minimal or inline editing. The same variants apply to every field component.
prefix and suffix accept icons, text or any element. They sit inside the field, so the whole box highlights on focus. Use text adornments for fixed parts of the value (a scheme, a domain, a currency) that the user shouldn't type.
A React Aria Button in the suffix toggles type between password and text. Give it an aria-label that describes the action, and set autoComplete so password managers recognise the field.
type, inputMode and autoComplete pick the right mobile keyboard and let browsers autofill. Turn off spellCheck and autoCorrect for identifiers like repository names or usernames.
isDisabled removes the field from the tab order and dims it. isReadOnly keeps it focusable and selectable, so users can still copy the value. Prefer read only for values users need but can't change.
Only owners can rename the workspace.
Read only: can be focused, selected and copied.
import { TextField } from "@/components/ui/text-field";export default function TextFieldDisabled() { return ( <div className="flex w-full max-w-xs flex-col gap-5"> <TextField label="Workspace" defaultValue="Acme Inc." isDisabled description="Only owners can rename the workspace." /> <TextField label="Workspace ID" defaultValue="ws_01HZX4Q9K2M7" isReadOnly description="Read only: can be focused, selected and copied." /> </div> );}
isRequired, type="email", pattern, minLength and maxLength use the browser's constraint validation. By default (validationBehavior="native") the form won't submit while a field is invalid, and errors appear after the user submits.
import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { TextField } from "@/components/ui/text-field";export default function TextFieldValidation() { return ( <Form className="flex w-full max-w-xs flex-col gap-4" onSubmit={(e) => e.preventDefault()} > <TextField label="Email" name="email" type="email" isRequired placeholder="you@company.com" /> <TextField label="Invite code" name="code" isRequired pattern="[A-Z]{4}-[0-9]{4}" placeholder="ABCD-1234" description="Four capital letters, a dash, then four digits." /> <Button type="submit" className="self-start"> Join workspace </Button> </Form> );}
validate receives the current value and returns an error string, an array of strings, or null / true when valid. With validationBehavior="aria" on the Form (or the field), errors show live as the user types and don't block submission.
import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { TextField } from "@/components/ui/text-field";const taken = ["admin", "support", "nischal"];export default function TextFieldCustomValidation() { return ( <Form className="flex w-full max-w-xs flex-col gap-4" validationBehavior="aria" onSubmit={(e) => e.preventDefault()} > <TextField label="Username" name="username" defaultValue="admin" prefix="@" description="Lowercase letters, numbers and dashes." validate={(value) => { if (!/^[a-z0-9-]*$/.test(value)) return "Use lowercase letters, numbers and dashes only."; if (value.length > 0 && value.length < 3) return "Must be at least 3 characters."; if (taken.includes(value)) return `@${value} is already taken.`; return null; }} /> <Button type="submit" className="self-start"> Claim username </Button> </Form> );}
Pass validationErrors to React Aria's Form, keyed by field name, to show errors returned by your API. Each error clears as soon as the user edits that field. For a single field, isInvalid with errorMessage does the same.
import { useState } from "react";import { Form } from "react-aria-components";import { Button } from "@/components/ui/button";import { TextField } from "@/components/ui/text-field";type Errors = Record<string, string>;async function signUp(data: FormData): Promise<Errors> { await new Promise((r) => setTimeout(r, 600)); const errors: Errors = {}; if (String(data.get("email")).endsWith("@example.com")) errors.email = "An account with this email already exists."; if (String(data.get("company")).trim().toLowerCase() === "acme") errors.company = "This workspace name is reserved."; return errors;}export default function TextFieldServerErrors() { const [errors, setErrors] = useState<Errors>({}); const [pending, setPending] = useState(false); return ( <Form className="flex w-full max-w-xs flex-col gap-4" validationErrors={errors} onSubmit={async (e) => { e.preventDefault(); setPending(true); setErrors(await signUp(new FormData(e.currentTarget))); setPending(false); }} > <TextField label="Email" name="email" type="email" isRequired defaultValue="jane@example.com" /> <TextField label="Company" name="company" isRequired defaultValue="Acme" /> <Button type="submit" isPending={pending} className="self-start"> Create workspace </Button> </Form> );}
When the props aren't enough, build the field from the parts in field.tsx inside React Aria's TextField. Here a label row with a plan hint and an inline action button. Add group/field to the root so the label reacts to the disabled and required states.
Pro plan
https://
Point a CNAME record at cname.acme.dev first.
import { TextField as TextFieldPrimitive } from "react-aria-components";import { Button } from "@/components/ui/button";import { Description, FieldAddon, FieldError, FieldGroup, FieldInput, Label,} from "@/components/ui/field";export default function TextFieldComposition() { return ( <TextFieldPrimitive className="group/field flex w-full max-w-sm flex-col gap-1.5" defaultValue="docs.acme.dev" isRequired > <div className="flex items-center justify-between"> <Label>Custom domain</Label> <span className="text-muted-foreground text-xs">Pro plan</span> </div> <FieldGroup className="pr-1"> <FieldAddon>https://</FieldAddon> <FieldInput placeholder="docs.example.com" /> <Button size="xs" variant="soft"> Verify </Button> </FieldGroup> <Description>Point a CNAME record at cname.acme.dev first.</Description> <FieldError /> </TextFieldPrimitive> );}
field.tsx exports the building blocks and their styles, shared by every field component:
tsx
import { bareInputStyles, // classes for an <input> inside a FieldGroup fieldVariants, // tailwind-variants for the box: { variant, size } inputVariants, // same look for a standalone <input>, keyed off data-focused} from "@/components/ui/field";