---
id: "packages/active-ui/field"
title: "Field"
description: "Label, control, description and error as one accessible form field, with its error, disabled and grouped states."
url: "/packages/active-ui/field"
locale: "en"
revision: "95e53d64110e2bd7"
---

# Field

`Field` and its parts give every form control the same structure: a label above, the control, an
optional description below, and an error when the value is not accepted. They do not validate:
your application decides what is valid and what the message says; the field shows it.

**Example: Label, control and description**

Client: Runs entirely in your browser. Source: `examples/ui/field-anatomy.tsx`.

```tsx
// apps/docs/examples/ui/field-anatomy.tsx
'use client';

import {
  Field,
  FieldDescription,
  FieldLabel,
} from '@workspace/active-ui/components/primitives/field';
import {Input} from '@workspace/active-ui/components/primitives/input';

/**
 * Label, control and description. `FieldLabel` does not associate itself:
 * `htmlFor` on the label and the same `id` on the control do. Clicking the
 * label moves the focus into the input.
 */
export default function FieldAnatomy() {
  return (
    <Field className="max-w-sm">
      <FieldLabel htmlFor="anatomy-email">Email</FieldLabel>
      <Input
        id="anatomy-email"
        type="email"
        placeholder="name@example.com"
        aria-describedby="anatomy-email-description"
      />
      <FieldDescription id="anatomy-email-description">
        Used for sign-in and for service notifications.
      </FieldDescription>
    </Field>
  );
}
```

## Usage

```tsx
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from '@workspace/active-ui/components/primitives/field';
import {Input} from '@workspace/active-ui/components/primitives/input';

<Field>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" aria-describedby="email-description" />
  <FieldDescription id="email-description">Used for sign-in.</FieldDescription>
</Field>;
```

## Anatomy

- `Field`: one control with its label, description and error. It renders `role="group"`.
- `FieldLabel`: the label. It does **not** associate itself: give it `htmlFor` and the control the
  same `id`.
- `FieldDescription`: help text below the control.
- `FieldError`: the error, announced as it appears (`role="alert"`).
- `FieldContent` and `FieldTitle`: label and description beside a control, in a horizontal field.
- `FieldSet`, `FieldLegend` and `FieldGroup`: a group of related fields under one legend.
- `FieldSeparator`: a divider between fields in a group.

## Validation and the error state

**Example: Validation and the error state**

Client: Runs entirely in your browser. Source: `examples/ui/field-validation.tsx`.

```tsx
// apps/docs/examples/ui/field-validation.tsx
'use client';

import {useState, type FormEvent} from 'react';

import {Button} from '@workspace/active-ui/components/primitives/button';
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from '@workspace/active-ui/components/primitives/field';
import {Input} from '@workspace/active-ui/components/primitives/input';

/** The application's own rule and message: the field only shows them. */
function validateEmail(value: string): string | null {
  if (value.trim() === '') return 'Enter your email address.';
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
    return 'Enter an email address like name@example.com.';
  }
  return null;
}

/**
 * Validation on submit, then on every change. `data-invalid` on `Field`
 * styles the whole field; `aria-invalid` on the control tells assistive
 * technology; `aria-describedby` points at the description and the error,
 * which `FieldError` announces (`role="alert"`).
 */
export default function FieldValidation() {
  const [value, setValue] = useState('');
  const [error, setError] = useState<string | null>(null);
  const [submitted, setSubmitted] = useState(false);
  const [sent, setSent] = useState(false);

  const onSubmit = (event: FormEvent) => {
    event.preventDefault();
    const message = validateEmail(value);
    setSubmitted(true);
    setError(message);
    setSent(message === null);
  };

  return (
    <form
      onSubmit={onSubmit}
      noValidate
      className="flex max-w-sm flex-col gap-4"
    >
      <Field data-invalid={error ? true : undefined}>
        <FieldLabel htmlFor="validation-email">
          Email
          <span aria-hidden className="text-destructive">
            *
          </span>
          <span className="sr-only">(required)</span>
        </FieldLabel>
        <Input
          id="validation-email"
          type="email"
          autoComplete="email"
          value={value}
          aria-invalid={error ? true : undefined}
          aria-describedby={
            error
              ? 'validation-email-description validation-email-error'
              : 'validation-email-description'
          }
          onChange={(event) => {
            setValue(event.target.value);
            setSent(false);
            if (submitted) setError(validateEmail(event.target.value));
          }}
        />
        <FieldDescription id="validation-email-description">
          We send the confirmation to this address.
        </FieldDescription>
        {error ? (
          <FieldError id="validation-email-error">{error}</FieldError>
        ) : null}
      </Field>
      <div className="flex items-center gap-3">
        <Button type="submit">Send confirmation</Button>
        {sent ? (
          <span role="status" className="text-sm">
            Sent to {value}.
          </span>
        ) : null}
      </div>
    </form>
  );
}
```

Three things together make an error visible and audible:

1. `data-invalid` on `Field` gives the label and the error the error colour;
2. `aria-invalid` on the control marks it as invalid for assistive technology and colours its
   border;
3. `aria-describedby` on the control names the description and the error, so a screen reader reads
   them with the field.

Show the error after the reader submits or leaves the field, not on the first keystroke. A
required field says so in its label; the `*` is hidden from screen readers, and a visually hidden
"(required)" is read instead.

## Other states

**Example: Disabled, several errors, horizontal and grouped**

Client: Runs entirely in your browser. Source: `examples/ui/field-states.tsx`.

```tsx
// apps/docs/examples/ui/field-states.tsx
'use client';

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSet,
  FieldTitle,
} from '@workspace/active-ui/components/primitives/field';
import {Input} from '@workspace/active-ui/components/primitives/input';
import {Switch} from '@workspace/active-ui/components/primitives/switch';

/**
 * Disabled, several errors at once, a horizontal row for a switch, and a
 * group of fields under one legend.
 */
export default function FieldStates() {
  return (
    <div className="grid w-full max-w-2xl gap-8 sm:grid-cols-2">
      <Field data-disabled>
        <FieldLabel htmlFor="states-organisation">Organisation</FieldLabel>
        <Input id="states-organisation" disabled defaultValue="Example Ltd" />
        <FieldDescription>Set when the account was created.</FieldDescription>
      </Field>

      <Field data-invalid>
        <FieldLabel htmlFor="states-password">Password</FieldLabel>
        <Input
          id="states-password"
          type="password"
          aria-invalid
          aria-describedby="states-password-errors"
          defaultValue="short"
        />
        <FieldError
          id="states-password-errors"
          errors={[
            {message: 'Use at least 12 characters.'},
            {message: 'Add at least one digit.'},
            {message: 'Use at least 12 characters.'},
          ]}
        />
      </Field>

      <Field orientation="horizontal">
        <FieldContent>
          <FieldTitle id="states-statement-title">Monthly statement</FieldTitle>
          <FieldDescription id="states-statement-description">
            One message on the first working day of the month.
          </FieldDescription>
        </FieldContent>
        <Switch
          aria-labelledby="states-statement-title"
          aria-describedby="states-statement-description"
        />
      </Field>

      <FieldSet>
        <FieldLegend>Address</FieldLegend>
        <FieldGroup>
          <Field>
            <FieldLabel htmlFor="states-street">Street</FieldLabel>
            <Input id="states-street" autoComplete="address-line1" />
          </Field>
          <Field>
            <FieldLabel htmlFor="states-city">City</FieldLabel>
            <Input id="states-city" autoComplete="address-level2" />
          </Field>
        </FieldGroup>
      </FieldSet>
    </div>
  );
}
```

- **Disabled:** `data-disabled` on `Field` dims the label; the control needs its own `disabled`.
- **Several errors:** pass `errors` to `FieldError`; it removes duplicates and shows a list from two
  messages on.
- **Horizontal:** `orientation="horizontal"` puts the control beside its label, for switches and
  checkboxes; `FieldContent` holds the title and description.
- **Grouped:** `FieldSet` renders a real `<fieldset>` with `FieldLegend` as its `<legend>`.

## What your application provides

- The validation rules and the messages, in the reader's language.
- The ids that tie label, control, description and error together.
- When to show an error: on submit, on leaving the field, or while typing.

## Accessibility and keyboard

- Clicking the label focuses its control when `htmlFor` and `id` match.
- `FieldError` is announced when it appears; keep it out of the page while there is no error.
- A disabled control is skipped by `Tab`.

## API

### Field

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `orientation` | `"horizontal" \| "responsive" \| "vertical" \| null` | no | How the label and the control are arranged. `vertical`: the label above the control. `horizontal`: the control beside its label, for a switch or a checkbox; put the title and description in `FieldContent`. `responsive`: vertical, and horizontal once the surrounding `FieldGroup` is at least the `md` container width. |

### FieldError

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `errors` | `({ message?: string; } \| undefined)[]` | no | The messages to show when there are no `children`, for example the errors of a form library. Duplicate messages are shown once; one message is shown as text, two or more as a list. An entry without a `message` is not shown. |

### FieldLegend

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `variant` | `"label" \| "legend"` | no | 'legend' for the heading of a group of fields; 'label' for a legend in the size of a field label, for example above a group of checkboxes. |

### Other parts

**FieldLabel**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

FieldLabel has no props of its own.

**FieldDescription**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

FieldDescription has no props of its own.

**FieldGroup**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

FieldGroup has no props of its own.

**FieldSeparator**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | no |  |

**FieldSet**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

FieldSet has no props of its own.

**FieldContent**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

FieldContent has no props of its own.

**FieldTitle**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

FieldTitle has no props of its own.

### Label

`FieldLabel` is built on `Label`, a styled `<label>`. Use `Label` on its own for a control
outside a field, such as a checkbox beside its text. It dims inside a disabled group
(`data-disabled="true"`), or after a disabled control that carries the `peer` class.

**Label**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

Label has no props of its own.

The controls themselves are documented on their own pages: [Input and Textarea](/packages/active-ui/input).

> [!NOTE]
> **Based on shadcn/ui**
>
> This page adapts the Field documentation of shadcn/ui (MIT licence) to this
> package; see [Third-party notices](/third-party-notices).
