Skip to Content

Field

Utility components for composing accessible form fields with labels, descriptions, error messages, and input grouping.

As shown on your NRIC or FIN.

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>

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.

Examples

Sizes

All field components accept a size prop ("xs", "sm", or "md") to match the size of the parent field.

Extra small description
Small description
Medium description

FieldError

When the parent form field has isInvalid set, FieldError becomes visible with the error message. String children automatically include an error icon.

Please enter a valid email address.

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.

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.

Composing a Custom Form

Combine field components with React Aria's Form and field wrappers to build custom forms with built-in validation.

This will be your public display name.

Validation

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.

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

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

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