# Field (/ui/components/react/field)



<Playground component="field" />

## Usage [#usage]

```tsx
import { Field, FieldLabel, FieldDescription, FieldError, FieldValidity } from '@appica/ui-react/field'
import { Fieldset, FieldsetLegend } from '@appica/ui-react/fieldset'
```

```tsx
<Field>
  <FieldLabel>Email</FieldLabel>
  <Input type="email" placeholder="you@example.com" />
  <FieldDescription>We'll send your receipt here.</FieldDescription>
</Field>
```

`Field` wires a label, description, and error message to one control without you having to juggle `id`/`htmlFor`/`aria-describedby` by hand. Drop any Appica form control inside it — [`Input`](/ui/components/react/input), [`Textarea`](/ui/components/react/textarea), [`NumberField`](/ui/components/react/number-field), [`Select`](/ui/components/react/select), [`Switch`](/ui/components/react/switch), and the rest are all Field-aware: they pick up the field's `disabled` and `invalid` state and contribute their value to validation.

Validation is built in. Give `Field` a `validate` function (or use the native constraints on the control) and render a `FieldError` to surface the message — no separate state needed. Use `Fieldset` to gather several related fields under a single legend, and to disable the whole group at once.

## Examples [#examples]

### Default [#default]

A label sits above the control and a description below it. `Field` associates all three automatically — clicking the label focuses the control, and the description is announced as the control's `aria-describedby`.

```tsx
import { Field, FieldLabel, FieldDescription } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'

export default function FieldDefault() {
  return (
    <Field className="w-full max-w-70">
      <FieldLabel>Email</FieldLabel>
      <Input type="email" placeholder="you@example.com" />
      <FieldDescription>We'll send your receipt here.</FieldDescription>
    </Field>
  )
}
```

### Validation [#validation]

Pass a `validate` callback that returns an error string (or an array of them) when the value is invalid, or `null` when it's fine. `validationMode` decides when it runs — `onBlur` here, or `onChange` / `onSubmit`. The returned message renders inside `FieldError`, and the control flips to its error styling via the field's `invalid` state.

```tsx
'use client'

import { Field, FieldLabel, FieldDescription, FieldError } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'

export default function FieldValidation() {
  return (
    <Field
      className="w-full max-w-70"
      validationMode="onBlur"
      validate={(value) => {
        const email = String(value).trim()
        if (!email) return 'Email is required'
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Enter a valid email address'
        return null
      }}
    >
      <FieldLabel>Email</FieldLabel>
      <Input type="email" placeholder="you@example.com" />
      <FieldDescription>Validates when the field loses focus.</FieldDescription>
      <FieldError />
    </Field>
  )
}
```

### Disabled & read-only [#disabled--read-only]

Set `disabled` on the `Field` to disable the whole thing at once — it greys the control and takes it out of the tab order, propagating down to whatever control is inside. `readOnly` is a control-level concern (there's no field-wide equivalent), so set it on the input: it stays focusable and selectable but can't be edited.

```tsx
import { Field, FieldLabel, FieldDescription } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'

export default function FieldDisabled() {
  return (
    <div className="flex w-full max-w-70 flex-col gap-5">
      <Field disabled>
        <FieldLabel>Email</FieldLabel>
        <Input type="email" defaultValue="you@example.com" />
        <FieldDescription>The whole field is disabled.</FieldDescription>
      </Field>
      <Field>
        <FieldLabel>API key</FieldLabel>
        <Input defaultValue="sk_live_8f2c91a4" readOnly />
        <FieldDescription>Focusable and selectable, but not editable.</FieldDescription>
      </Field>
    </div>
  )
}
```

### Fieldset [#fieldset]

`Fieldset` groups related fields under a `FieldsetLegend` and renders a native `<fieldset>` / `<legend>`. Setting `disabled` on the fieldset disables every control inside it in one move.

```tsx
import { Fieldset, FieldsetLegend } from '@appica/ui-react/fieldset'
import { Field, FieldLabel } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'

export default function FieldFieldset() {
  return (
    <Fieldset className="w-full max-w-70">
      <FieldsetLegend>Shipping address</FieldsetLegend>
      <Field>
        <FieldLabel>Street</FieldLabel>
        <Input placeholder="123 Main St" />
      </Field>
      <Field>
        <FieldLabel>City</FieldLabel>
        <Input placeholder="Springfield" />
      </Field>
      <Field>
        <FieldLabel>Postal code</FieldLabel>
        <Input placeholder="12345" />
      </Field>
    </Fieldset>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior — roving focus, popup placement, and the like — follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The label and description align to the start edge and the control follows the resolved direction. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="field" />

## API reference [#api-reference]

`Field` and `Fieldset` wrap [Base UI's Field](https://base-ui.com/react/components/field) and [Fieldset](https://base-ui.com/react/components/fieldset); every remaining prop is forwarded to the underlying element.

### Field [#field]

The root that provides labelling and validation context to a single control. Renders a `<div>` and exposes `data-valid` / `data-invalid` / `data-dirty` / `data-touched` / `data-filled` / `data-focused` / `data-disabled` for styling.

| Prop                     | <ColMinWidth width="200">Type</ColMinWidth>                                | Default      | <ColMinWidth width="220">Description</ColMinWidth>                              |
| ------------------------ | -------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------- |
| `name`                   | `string`                                                                   | —            | Field name on form submission; takes precedence over the control's own `name`.  |
| `validate`               | <Code>(value, formValues) => string \| string\[] \| Promise \| null</Code> | —            | Returns error message(s) when invalid, or `null` when valid. May be async.      |
| `validationMode`         | <Code>'onSubmit' \| 'onBlur' \| 'onChange'</Code>                          | `'onSubmit'` | When `validate` runs.                                                           |
| `validationDebounceTime` | `number`                                                                   | `0`          | Milliseconds to debounce `validate` in `onChange` mode.                         |
| `invalid`                | `boolean`                                                                  | —            | Force the invalid state — handy when an external library owns validation.       |
| `disabled`               | `boolean`                                                                  | `false`      | Disable the control inside; takes precedence over the control's own `disabled`. |
| `dirty`                  | `boolean`                                                                  | —            | Externally control whether the value differs from its initial value.            |
| `touched`                | `boolean`                                                                  | —            | Externally control whether the field has been interacted with.                  |
| `render`                 | <Code>ReactElement \| ((props, state) => ReactElement)</Code>              | —            | Replace the element, or compose it with another component.                      |
| `className`              | <Code>string \| ((state) => string)</Code>                                 | —            | Extra classes, merged via `tailwind-merge`. May be a function of state.         |

### FieldLabel [#fieldlabel]

The control's label. Renders a `<label>` tied to the control, so clicking it moves focus.

| Prop          | <ColMinWidth width="200">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth>                                            |
| ------------- | ------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------- |
| `nativeLabel` | `boolean`                                                     | `true`  | Set `false` when rendering a non-`<label>` element through `render` (e.g. labelling a group). |
| `render`      | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace the element, or compose it with another component.                                    |
| `className`   | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.                                                   |

### FieldDescription [#fielddescription]

Supporting text for the control. Renders a `<p>` and is wired to the control's `aria-describedby`.

| Prop        | <ColMinWidth width="200">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth>         |
| ----------- | ------------------------------------------------------------- | ------- | ---------------------------------------------------------- |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace the element, or compose it with another component. |
| `className` | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.                |

### FieldError [#fielderror]

The validation message. Renders only when the field is invalid (or `match` says so), with a height/opacity enter–exit transition that honours `prefers-reduced-motion`. With no children it shows the field's current error message; with `match` you supply custom children for a specific validity condition.

| Prop        | <ColMinWidth width="200">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth>                                              |
| ----------- | ------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------- |
| `match`     | <Code>boolean \| keyof ValidityState</Code>                   | —       | `true` always shows; a `ValidityState` key (e.g. `'valueMissing'`) shows only for that failure. |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace the element, or compose it with another component.                                      |
| `className` | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`. An `svg` child is auto-sized and aligned.           |

### FieldValidity [#fieldvalidity]

A render-prop that exposes the field's live validity so you can render fully custom messaging.

| Prop       | <ColMinWidth width="200">Type</ColMinWidth> | Default | <ColMinWidth width="220">Description</ColMinWidth>                            |
| ---------- | ------------------------------------------- | ------- | ----------------------------------------------------------------------------- |
| `children` | <Code>(state) => ReactNode</Code>           | —       | Receives `{ validity, value, error, errors }` and returns the node to render. |

### Fieldset [#fieldset-1]

Groups related fields. Renders a native `<fieldset>` (`role="group"`) laid out as a column.

| Prop        | <ColMinWidth width="200">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth>         |
| ----------- | ------------------------------------------------------------- | ------- | ---------------------------------------------------------- |
| `disabled`  | `boolean`                                                     | `false` | Disables every control inside the fieldset.                |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace the element, or compose it with another component. |
| `className` | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.                |

### FieldsetLegend [#fieldsetlegend]

The fieldset's heading. Renders a `<legend>` that names the group.

| Prop        | <ColMinWidth width="200">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth>         |
| ----------- | ------------------------------------------------------------- | ------- | ---------------------------------------------------------- |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace the element, or compose it with another component. |
| `className` | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.                |

## Accessibility [#accessibility]

* `FieldLabel` renders a real `<label>` bound to the control, so the label is both clickable and announced — no manual `htmlFor`/`id` wiring.
* `FieldDescription` is linked through the control's `aria-describedby`, and `FieldError` is announced when it appears.
* `Fieldset` renders a native `<fieldset>` with `role="group"` named by its `<legend>`; `disabled` on it disables every descendant control.
* The error message's enter–exit transition honours `prefers-reduced-motion`.
* To label a group of controls (a `RadioGroup`, `ToggleGroup`, or a `DateField`/`TimeField` segment group) rather than a single input, don't use `FieldLabel` — a `<label for>` doesn't associate with a `role="group"`. Name those with `aria-labelledby` pointing at a heading instead.
