Field
Primitives for composing accessible form fields with labels, descriptions, errors, and input grouping. Most callers should use a higher-level component (TextField, etc.) instead.
import { TextField as AriaTextField } from "react-aria-components"import { Description, FieldError, Input, Label } from "@opengovsg/oui"export const Example = () => { return ( <AriaTextField className="flex flex-col gap-2"> <Label>Full name</Label> <Input placeholder="Enter your full name" /> <Description>As shown on your NRIC or FIN.</Description> <FieldError /> </AriaTextField> )}Usage
import {
Description,
FieldError,
FieldErrorIcon,
FieldGroup,
Label,
} from "@opengovsg/oui"<AriaTextField className="flex flex-col gap-2">
<Label>Full name</Label>
<Input placeholder="Enter your full name" />
<Description>As shown on your NRIC or FIN.</Description>
<FieldError />
</AriaTextField>Alternatively, install the component as local source via the shadcn CLI:
npx shadcn@latest add https://oui.open.gov.sg/r/field.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/field.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/field.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/field.jsonThe Field module provides low-level building blocks used internally by the higher-level field components in OUI. In most cases, you should use one of these components instead of using Field primitives directly:
- CheckboxGroup — Group of checkbox options
- ComboBox — Type-ahead filterable select input
- DateField — Date input (keyboard only)
- DatePicker — Date input with calendar popover
- DateRangePicker — Date range input with calendar popover
- NumberField — Numeric input with steppers
- PhoneNumberField — Phone number input
- RadioGroup — Group of radio button options
- SearchField — Search input with clear button
- Select — Non-filterable dropdown for single selection
- TagField — Multi-select input with filtering
- TextAreaField — Multi-line text input
- TextField — Single-line text input
- TimeField — Time input
These higher-level components already compose the Field primitives with the correct structure, styling, and accessibility wiring.
Only use the Field primitives directly if you need to build a custom form field that is not covered by the components above.
OUI exports 5 field-related primitives:
- Label: A label element that automatically associates with its parent form field via
forandaria-labelledby. Built on React Aria's Label. - Description: A text element rendered with
slot="description"that provides supplementary context for a form field, linked viaaria-describedby. Built on React Aria's Text. - FieldError: Displays validation error messages with custom styling instead of browser defaults. Automatically shows a
CircleAlerticon when given a string child. Supports a render prop for custom error messages based on ValidityState. Built on React Aria's FieldError. - FieldErrorIcon: The error icon used inside
FieldError. Can be used standalone for custom error layouts. - FieldGroup: A styled group container for composing inputs with adornments (icons, prefixes, etc.). Provides render props for interactive state styling. Built on React Aria's Group.
Note: These primitives must be placed inside a React Aria form field wrapper (e.g.,
TextField,NumberFieldfromreact-aria-components) to function correctly. The wrapper provides the accessibility relationships between the label, description, error, and input elements. If you're not building a custom field, use one of the higher-level components listed above instead.
Anatomy
The field primitives compose as follows:
- React Aria field wrapper (e.g.,
AriaTextField,AriaNumberField) — provides accessibility context- Label — the
<label>element; automatically associated with the field input - Input — the form control (supplied by the caller or a higher-level component)
- Description — supplementary helper text; linked via
aria-describedby - FieldError — error message container; becomes visible when the field is invalid
- FieldErrorIcon — the alert circle icon; can be used standalone in custom error layouts
- FieldGroup — optional bordered container for inputs with adornments
- Label — the
Examples
Sizes
All field components accept a size prop ("xs", "sm", or "md") to match the size of the parent field.
import { TextField as AriaTextField } from "react-aria-components"import { Description, Input, Label } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-col gap-6"> <AriaTextField className="flex flex-col gap-2"> <Label size="xs">Extra small label</Label> <Input size="xs" placeholder="Extra small" /> <Description size="xs">Extra small description</Description> </AriaTextField> <AriaTextField className="flex flex-col gap-2"> <Label size="sm">Small label</Label> <Input size="sm" placeholder="Small" /> <Description size="sm">Small description</Description> </AriaTextField> <AriaTextField className="flex flex-col gap-2"> <Label size="md">Medium label (default)</Label> <Input size="md" placeholder="Medium" /> <Description size="md">Medium description</Description> </AriaTextField> </div> )}FieldError
When the parent form field has isInvalid set, FieldError becomes visible with the error message. String children automatically include an error icon.
import { TextField as AriaTextField } from "react-aria-components"import { FieldError, Input, Label } from "@opengovsg/oui"export const Example = () => { return ( <AriaTextField isInvalid className="flex flex-col gap-2"> <Label>Email address</Label> <Input placeholder="Enter email" /> <FieldError>Please enter a valid email address.</FieldError> </AriaTextField> )}Custom Error Messages
FieldError accepts a render prop function as children, receiving validationErrors (an array of error strings) and validationDetails (a ValidityState object). Use this to customize error messages based on the specific validation failure.
When using the render prop, the error icon is not automatically included — use FieldErrorIcon to add it manually.
import { TextField as AriaTextField, Form } from "react-aria-components"import { Button, FieldError, FieldErrorIcon, Input, Label,} from "@opengovsg/oui"export const Example = () => { return ( <Form className="flex flex-col gap-4"> <AriaTextField name="email" type="email" isRequired className="flex flex-col gap-2" > <Label>Email</Label> <Input placeholder="Enter your email" /> <FieldError> {({ validationDetails }) => ( <> <FieldErrorIcon /> {validationDetails.valueMissing ? "Please enter an email address." : "Please enter a valid email address."} </> )} </FieldError> </AriaTextField> <Button type="submit" className="w-fit"> Submit </Button> </Form> )}FieldGroup
FieldGroup provides a styled container with border, focus ring, and invalid state styles. Use it to compose inputs with adornments like icons or text prefixes.
When using FieldGroup, set the Input variant to "unstyled" to avoid double-styling the border and focus ring.
import { Search } from "lucide-react"import { TextField as AriaTextField } from "react-aria-components"import { FieldGroup, Input, Label } from "@opengovsg/oui"export const Example = () => { return ( <AriaTextField className="flex flex-col gap-2"> <Label>Search</Label> <FieldGroup> <Search className="text-base-content-medium ml-3 h-4 w-4 shrink-0" /> <Input variant="unstyled" placeholder="Search for something" /> </FieldGroup> </AriaTextField> )}Composing a Custom Form
Combine field components with React Aria's Form and field wrappers to build custom forms with built-in validation.
import { TextField as AriaTextField, Form } from "react-aria-components"import { Button, Description, FieldError, FieldGroup, Input, Label,} from "@opengovsg/oui"export const Example = () => { return ( <Form className="flex flex-col gap-6"> <AriaTextField isRequired className="flex flex-col gap-2"> <Label>Username</Label> <FieldGroup> <span className="text-base-content-medium ml-3 text-sm">@</span> <Input variant="unstyled" placeholder="Enter username" /> </FieldGroup> <Description>This will be your public display name.</Description> <FieldError /> </AriaTextField> <AriaTextField isRequired className="flex flex-col gap-2"> <Label>Email</Label> <Input type="email" placeholder="you@example.com" /> <FieldError /> </AriaTextField> <Button type="submit" className="w-fit"> Submit </Button> </Form> )}Validation
See the Forms guide for end-to-end patterns including React Hook Form + Zod.
These field components work with React Aria's form validation system. Validation is configured on the parent form field wrapper (e.g., TextField, NumberField from react-aria-components), and FieldError displays the resulting error messages.
Built-in Constraint Validation
React Aria form field wrappers support native HTML constraint validation props such as isRequired, minLength, maxLength, pattern, and type. Errors are displayed after the user commits a value (on blur) or submits the form.
<AriaTextField isRequired minLength={3} className="flex flex-col gap-2">
<Label>Username</Label>
<Input />
<FieldError />
</AriaTextField>Custom Validation
Use the validate prop on the parent field wrapper for custom validation logic. The function receives the current value and returns an error message string (or null/true if valid).
<AriaTextField
validate={(value) =>
value.includes("@") ? null : "Username cannot contain @"
}
className="flex flex-col gap-2"
>
<Label>Username</Label>
<Input />
<FieldError />
</AriaTextField>Realtime Validation
For immediate feedback as the user types (e.g., password strength), use the isInvalid prop with a controlled error message instead of the built-in validation system.
<AriaTextField isInvalid={!isValid} className="flex flex-col gap-2">
<Label>Password</Label>
<Input />
<FieldError>Password must be at least 8 characters.</FieldError>
</AriaTextField>Validation Behavior
The validationBehavior prop on the parent field wrapper controls how validation is enforced:
"native"(default) — Uses native HTML form validation. Prevents form submission when fields are invalid."aria"— Only marks fields as invalid via ARIA attributes. Allows form submission regardless of validation state. Use this when integrating with external validation libraries such as React Hook Form or Zod.
Accessibility
Labelis automatically associated with its parent form field input viaforandaria-labelledby, managed by React Aria.Descriptionis announced by screen readers when the field receives focus, linked viaaria-describedby.FieldErroris announced when the field enters an invalid state, providing immediate feedback to screen reader users viaaria-describedby.- If a form field does not have a visible
Label, anaria-labeloraria-labelledbyprop must be passed to the parent field wrapper to identify it to assistive technology. - All components inherit their accessibility semantics from their React Aria counterparts.
Slots
Slots are named regions of the component you can target with custom Tailwind classes via the classNames prop. Each slot below corresponds to a key on the classNames object.
Only FieldError exposes multiple named slots. The other primitives (Label, Description, FieldErrorIcon, FieldGroup) accept a single className string.
FieldError slots (via FieldError's classNames):
icon: TheCircleAlerticon element rendered before the error text.text: The error message text container element.
Props
Label
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | The label content |
size | "xs" | "sm" | "md" | "md" | The size of the label text |
className | string | - | Additional CSS classes |
Description
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | The description content |
size | "xs" | "sm" | "md" | "md" | The size of the description text |
className | string | - | Additional CSS classes |
FieldError
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | ((validation: { validationErrors: string[], validationDetails: ValidityState }) => React.ReactNode) | - | The error message. Strings get an automatic icon. Accepts a render prop for custom error messages. |
size | "xs" | "sm" | "md" | "md" | The size of the error text and icon |
className | string | - | Additional CSS classes for the text slot |
classNames | SlotsToClasses<FieldErrorSlots> | - | Custom CSS classes for icon and text slots |
FieldError render-prop argument
When children is a function, it receives an object with:
| Property | Type | Description |
|---|---|---|
validationErrors | string[] | List of validation error strings from the field |
validationDetails | ValidityState | The native HTML ValidityState object for the field |
FieldErrorIcon
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "md" | The size of the icon |
className | string | - | Additional CSS classes |
FieldGroup
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | ((renderProps: GroupRenderProps) => React.ReactNode) | - | The group content. Accepts a render prop for access to interactive state. |
className | string | - | Additional CSS classes |
Inherits all remaining props from React Aria's Group. Notable ones: isDisabled, isInvalid, isReadOnly, aria-label.
FieldGroup Render Props
When passing a function as children to FieldGroup, it receives a GroupRenderProps object with the following properties:
| Property | Type | Description |
|---|---|---|
isFocused | boolean | Whether the group or any of its children is focused |
isFocusVisible | boolean | Whether focus is visible (keyboard navigation) |
isHovered | boolean | Whether the group is being hovered |
isDisabled | boolean | Whether the group is disabled |
isInvalid | boolean | Whether the group is in an invalid state |
isReadOnly | boolean | Whether the group is read-only |