Forms
TextField
A labeled one-line text input on the Field contract: pass label, and error from your server action or client check. The type sets the phone keyboard and autofill, password gets a show/hide button that keeps focus in the input, clearable adds a clear button, and startAdornment/endAdornment put text or icons inside the box.
In Spoonjoy
Spoonjoy, a recipe app, is built with Ouro HX (how).
See it live, no sign-in needed: Recipes · Search · API playground
In its source: app/components/recipe/ManualIngredientInput.tsx · app/components/recipe/ParsedIngredientRow.tsx · app/routes/my-recipes.tsx
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/text-field#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/text-fieldIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/text-field#v1.2.13 ourostack/ouro-ui/text-field-tests#v1.2.13import { TextField } from "@/components/ouro/text-field/text-field";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<TextField label="Email" type="email" name="email" required error={errors?.email} />
<TextField label="Password" type="password" name="password" autoComplete="new-password" />
<TextField label="Search recipes" hideLabel type="search" clearable startAdornment={<SearchIcon />} />
<TextField label="Price" startAdornment="$" endAdornment="per jar" />
// A hero search: the row draws the rules, the field has no frame and large text.
<form action="/recipes" className="flex items-center gap-3 border-y border-border font-serif">
<SearchIcon aria-hidden="true" className="size-5 shrink-0" />
<TextField label="Search recipes" hideLabel type="search" name="q" placeholder="tomato, beans, lemon" variant="plain" size="lg" className="flex-1" />
<Button type="submit">Search</Button>
</form>Usage
Not here, because each has its own component:
- Numbers:
NumberFieldparses, steps and formats them. A digit code (a PIN, a postcode) stays a TextField withinputMode="numeric", or is anOtpField.
- A date picked from a calendar:
DatePicker.type="date"here opens the OS picker.
- A date and time together: there is no
datetime-local. Use aDatePickerand atype="time"TextField side by side, and join the two values on the server.
- Files:
FileField.
- Several lines:
TextArea.
variant="plain" draws no frame, and size="lg" sets 20px text: together they put the field inside a row your app frames, such as a page's main search between two rules.
Use instead when
- Use
TextAreainstead for several lines,NumberFieldfor numbers,DatePickerfor a date picked from a calendar,FileFieldfor files, andFieldwithFieldControlwhen the control is not a text input.
Props
Type: TextFieldProps.
className- Type:
string - Layout only: margins, width, grid placement (
mt-4,col-span-2). Appearance classes warn in development; style fields in your theme. clearable- Type:
boolean - Adds a button that empties the field and puts focus back in it. It shows while there is text.
clearLabel- Type:
string - Default:
"Clear" - The name of the clear button that
clearableadds, as visually hidden text. Translate it with your app. description- Type:
ReactNode - Supporting text under the label, read after the label through
aria-describedby. disabled- Type:
boolean - Disables the field, its buttons included, and fades it.
endAdornment- Type:
ReactNode - Text or an icon inside the box, after the input:
"kg",<CheckIcon />. Text is also read as part of the description. error- Type:
ReadonlyErrorMessages - What is wrong, from your server action or client check. The message (or several, as a list) shows under the field, which gets
aria-invalid. A message is text or a React node (one with a link); an empty string or array means valid. hideLabel- Type:
boolean - Hides the label visually; screen readers still read it as the field's name.
label(required)- Type:
string - What the field is for, shown above it and read as its name. Required: every control needs a name. To hide it visually, keep it and add
hideLabel. A placeholder is not a label. name- Type:
string - The form field name the value submits under.
placeholder- Type:
string - An example of what to type, shown while the field is empty:
"you@example.com". It is not a label; passlabel, which stays visible. ref- Type:
Ref<HTMLInputElement> - The input element.
required- Type:
boolean - The field must be filled: it gets
requiredandaria-required, and the label a marker. showPasswordLabel- Type:
string - Default:
"Show password" - The name of a password field's show/hide button, as visually hidden text and not an
aria-label, so a test'sgetByLabel("Password")finds the input alone; find the button withgetByRole("button", { name: "Show password" }). It stays the same while the password shows; the button's pressed state (aria-pressed) says whether it does. Translate it with your app. size- Type:
"md" | "lg" - Default:
"md" - Control size.
mdis 44px tall with 16px text on phones and steps down to desktop density from thesmbreakpoint (640px).lgis 56px tall (48px on desktop) with 20px text at every width, for a page's main search. The font family is inherited, so set a display font on a parent. It replaces the input's ownsizeattribute (a width in characters); set the width withclassName. startAdornment- Type:
ReactNode - Text or an icon inside the box, before the input:
"$",<SearchIcon />. Text is also read as part of the field's description. Replaces shadcn's InputGroup. type- Type:
"text" | "url" | "date" | "time" | "search" | "email" | "password" | "tel" - Default:
"text" - What kind of text it holds. Each type sets the phone keyboard (
inputMode) and autofill (autoComplete) for you; pass either to override.passwordadds a show/hide button;dateandtimeopen the OS picker. Other kinds of value have their own component:NumberFieldfor numbers (inputMode="numeric"here for digit codes),DatePickerfor a date picked from a calendar,DatePickerbeside atype="time"TextField for a date and time (there is nodatetime-local), andFileFieldfor files. Any other string is a type error whose message names them. variant- Type:
"outline" | "plain" - Default:
"outline" - The frame.
outlinedraws the bordered box.plaindraws no border, background or side padding, for a field inside a frame your app draws (a search row with its own rules and a button beside the input); it keeps the focus outline, and the error message still shows under it, but with no border to turn red, color your own frame for an invalid field. Any other string is a type error whose message names the fix.
Native attributes
Also accepts the attributes in React's HTMLAttributes and InputHTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
endIcon: Not a prop. Ouro UI usesendAdornment:<TextField endAdornment={<CheckIcon />}>.errorMessage: Not a prop. Ouro UI useserror:<TextField error="Required.">.helperText: Not a prop. Ouro UI usesdescription:<TextField description="We never share it.">.invalid: Not a prop.errormarks the field invalid:<TextField error="What is wrong">.isClearable: Not a prop. Ouro UI usesclearable:<TextField clearable>.isDisabled: Not a prop. Ouro UI usesdisabled:<TextField disabled>.isInvalid: Not a prop.errormarks the field invalid:<TextField error="What is wrong">.isRequired: Not a prop. Ouro UI usesrequired:<TextField required>.leftIcon: Not a prop. Ouro UI usesstartAdornment:<TextField startAdornment={<SearchIcon />}>.multiline: Not a prop. TextField is one line; useTextAreafor several:<TextArea label="Notes" autoGrow />.prefix: Not a prop. Ouro UI usesstartAdornment:<TextField startAdornment="$">.rightIcon: Not a prop. Ouro UI usesendAdornment:<TextField endAdornment={<CheckIcon />}>.startIcon: Not a prop. Ouro UI usesstartAdornment:<TextField startAdornment={<SearchIcon />}>.suffix: Not a prop. Ouro UI usesendAdornment:<TextField endAdornment="kg">.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: TextField merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
ErrorMessage
One error message: text, or a React element for a message that carries a link or other markup.
type ErrorMessage = string | ReactElement;ReadonlyErrorMessages
A read-only message view accepted by fields and Form; mutable input arrays remain valid.
type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];Claim sources
- layout-classes: test:registry/ouro/components/ouro/text-field/text-field.test.tsx::TextField > lets app layout classes win on the root
verify:testExists(registry/ouro/components/ouro/text-field/text-field.test.tsx::TextField > lets app layout classes win on the root)