Open UI

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.json
pnpm dlx shadcn@latest add https://oui.open.gov.sg/r/field.json
npx shadcn@latest add https://oui.open.gov.sg/r/field.json
bunx --bun shadcn@latest add https://oui.open.gov.sg/r/field.json

The 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:

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 for and aria-labelledby. Built on React Aria's Label.
  • Description: A text element rendered with slot="description" that provides supplementary context for a form field, linked via aria-describedby. Built on React Aria's Text.
  • FieldError: Displays validation error messages with custom styling instead of browser defaults. Automatically shows a CircleAlert icon 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, NumberField from react-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

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

  • Label is automatically associated with its parent form field input via for and aria-labelledby, managed by React Aria.
  • Description is announced by screen readers when the field receives focus, linked via aria-describedby.
  • FieldError is announced when the field enters an invalid state, providing immediate feedback to screen reader users via aria-describedby.
  • If a form field does not have a visible Label, an aria-label or aria-labelledby prop 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: The CircleAlert icon element rendered before the error text.
  • text: The error message text container element.

Props

Label

PropTypeDefaultDescription
childrenReact.ReactNode-The label content
size"xs" | "sm" | "md""md"The size of the label text
classNamestring-Additional CSS classes

Description

PropTypeDefaultDescription
childrenReact.ReactNode-The description content
size"xs" | "sm" | "md""md"The size of the description text
classNamestring-Additional CSS classes

FieldError

PropTypeDefaultDescription
childrenReact.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
classNamestring-Additional CSS classes for the text slot
classNamesSlotsToClasses<FieldErrorSlots>-Custom CSS classes for icon and text slots

FieldError render-prop argument

When children is a function, it receives an object with:

PropertyTypeDescription
validationErrorsstring[]List of validation error strings from the field
validationDetailsValidityStateThe native HTML ValidityState object for the field

FieldErrorIcon

PropTypeDefaultDescription
size"xs" | "sm" | "md""md"The size of the icon
classNamestring-Additional CSS classes

FieldGroup

PropTypeDefaultDescription
childrenReact.ReactNode | ((renderProps: GroupRenderProps) => React.ReactNode)-The group content. Accepts a render prop for access to interactive state.
classNamestring-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:

PropertyTypeDescription
isFocusedbooleanWhether the group or any of its children is focused
isFocusVisiblebooleanWhether focus is visible (keyboard navigation)
isHoveredbooleanWhether the group is being hovered
isDisabledbooleanWhether the group is disabled
isInvalidbooleanWhether the group is in an invalid state
isReadOnlybooleanWhether the group is read-only

On this page