Forms
Field
A labeled form control: the label, an optional description, the control and its error, wired together with ids that match between the server and the browser. Every Ouro form control takes these props and is built on the same parts.
In Spoonjoy
Spoonjoy, a recipe app, is built with Ouro HX (how).
In its source: app/components/recipe/ManualIngredientInput.tsx · app/components/recipe/ParsedIngredientRow.tsx · app/components/recipe/StepEditorCard.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/field#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/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/field#v1.2.13 ourostack/ouro-ui/field-tests#v1.2.13import { Field } from "@/components/ouro/field/field";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<Field label="Email" name="email" required description="We never share it." error={errors?.email}>
<FieldControl type="email" autoComplete="email" />
</Field>
// Helper text after the control: a FieldDescription placed where it shows.
<Field label="Password" name="password" required error={errors?.password}>
<FieldControl type="password" autoComplete="new-password" />
<FieldDescription>Use 8 or more characters.</FieldDescription>
</Field>Usage
Pass error straight from a server action or a client check: it shows the message and sets aria-invalid. The error only shows: it never blocks the form's next submit, so the user can always send the form again (only the browser's own checks, such as required, block). Inside a Form, a field with a name shows the form's error for that name when it has no error of its own. The control is FieldControl, from field-parts.
The order is fixed: label, description, control, error. To put helper text after the control, leave description out and render a FieldDescription after the FieldControl. It is linked to the control (aria-describedby) wherever it sits, and the error still comes last. TextField and the other one-tag controls render their own control, so build that field from Field and FieldControl.
The required marker is drawn with CSS beside the label and is not part of its text, so a test finds the field by the label alone: getByLabelText("Email"), or Playwright's getByLabel("Email", { exact: true }). The marker stays visible in forced colors.
A field's error is the control's description, not an alert, so screen readers read it when focus reaches the field (a Form moves focus to the first invalid one). In a test, assert it with toHaveAccessibleDescription("Enter an email address.") or getByText; getByRole("alert") finds only a Form's message about the whole form.
Use instead when
- Use
Fieldsetinstead to group several controls under one legend, and theFieldRootparts when the label, control and error need another layout.
Props
Type: FieldProps.
children(required)- Type:
ReactNode - The control:
<FieldControl />, or<FieldControl render={<textarea />} />for another element. className- Type:
string - Layout only: margins, width, grid placement (
mt-4,col-span-2). Appearance classes warn in development; style fields in your theme. description- Type:
ReactNode - Supporting text under the label, read after the label through
aria-describedby. disabled- Type:
boolean - Disables the control and fades the field.
error- Type:
ReadonlyErrorMessages - What is wrong, from your server action or client check. A message (or several, as an array, each shown once as a list item) shows under the control, which gets
aria-invalidand points at the message witharia-describedby. A message is text or a React node, so it can carry a link:error={<>Already imported. <Link href="/recipes/soup">Open it</Link></>}. An empty string or array means valid. It only shows: it never blocks the next submit of the form around it. hideLabel- Type:
boolean - Hides the label visually; screen readers still read it as the control's name.
label(required)- Type:
string - What the control 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. name- Type:
string - The form field name the control submits under. A
Form'serrorsfor this name show here when there is noerrorprop, and go away once the user changes the control. required- Type:
boolean - The control must be filled: it gets
requiredandaria-required, and the label a marker.
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.
errorMessage: Not a prop. Ouro UI useserror:<Field error="Required.">.helperText: Not a prop. Ouro UI usesdescription:<Field description="We never share it.">.htmlFor: Not a prop. Field connects the label to its control itself:<Field label="Email"><FieldControl /></Field>.invalid: Not a prop.errormarks the field invalid:<Field error="What is wrong">.isDisabled: Not a prop. Ouro UI usesdisabled:<Field disabled>.isInvalid: Not a prop.errormarks the field invalid:<Field error="What is wrong">.isRequired: Not a prop. Ouro UI usesrequired:<Field required>.placeholder: Not a prop, and not a label. Passlabel, and a placeholder example on the control:<FieldControl placeholder="you@example.com" />.
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: Field 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.
Parts
When these props cannot express your layout, build it from the parts in @/components/ouro/field/field-parts. The nesting they need follows.
import {
FieldControl,
FieldDescription,
FieldError,
FieldLabel,
FieldRoot,
} from "@/components/ouro/field/field-parts";Required nesting
<FieldRoot name="email" required>
<FieldLabel>Email</FieldLabel>
<FieldDescription>We never share it.</FieldDescription>
<FieldControl type="email" />
<FieldError />
</FieldRoot>Each part checks that it sits inside <FieldRoot>: outside it, a part throws in development with a message that shows this JSX shape.field-nesting
FieldControl- What it is: The control, a text
<input>by default. It takes the field's id,name,disabledandrequired,aria-invalidfrom its error, andaria-describedbyfrom its description and error. Render another element withrender={<textarea />}. FieldDescription- What it is: Supporting text for the control, read after its label through
aria-describedby. FieldError- What it is: The error message, linked to the control through
aria-describedby. Without children it shows the browser's validation message, or a Base UIFormerror for the field'sname; withmatchand children it always shows them. It fades and slides in. FieldLabel- What it is: The control's label. It points at the control by id. A required field shows a marker after it, drawn by CSS on the element around the label, so the label's text and the control's name stay exactly what you wrote (screen readers hear "required" from the control).
classNamegoes on that element, where[data-ouro-required]is too; hide the label visually withclassName="sr-only".style,id,data-*attributes and event handlers go on the<label>. FieldRoot- What it is: The state owner of a field built from parts. It connects the label, description and error to the control, and passes
name,disabledandrequireddown. Use theFieldcomponent instead unless its props cannot express your layout; every Ouro form control is built on these parts. Parts link the description and error after hydration; useFieldwhen the server HTML must carry them, or passids andaria-describedbyyourself.
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/field/field.test.tsx::Field wiring > lets app layout classes win
verify:testExists(registry/ouro/components/ouro/field/field.test.tsx::Field wiring > lets app layout classes win)