# Field (/docs/field)





`Field` is the one place the form family wires accessible names and descriptions. Wrap a **field-aware control** — the kit's [`Input`](/docs/input), `Select`, `Combobox`, and the rest — and Ark UI hands it the right `id`, `aria-describedby`, `aria-invalid`, `disabled`, and `required` through context. No per-control plumbing, no manual `htmlFor`.

```tsx
import { Field } from '@42-central/ui-react/field';
import { Input } from '@42-central/ui-react/input';

<Field label="Email" description="We'll never share it.">
  <Input placeholder="login@student.42.fr" />
</Field>
```

<Callout type="info" title="Most controls don't need this">
  Every control in the form family — [`TextInput`](/docs/text-input),
  [`Textarea`](/docs/textarea), [`Select`](/docs/select),
  [`NumberInput`](/docs/number-input) and the rest — already carries `label` /
  `description` / `error` / `required` of its own, wired through this exact
  component. Reach for `Field` directly when you're composing something it
  doesn't cover: the bare [`Input`](/docs/input) shell, several controls sharing
  one label, or a control of your own.
</Callout>

## Playground [#playground]

<story.Playground />

## Anatomy [#anatomy]

The label binds to the control (click it to focus); the description renders **above** the control, between it and the label. An error renders **below** the control instead, independently of the description — set both at once and both stay visible.

<Preview align="block" code="&#x22;<Field\n  label=\&#x22;Email\&#x22;\n  description=\&#x22;We'll never share it.\&#x22;\n>\n  <Input placeholder=\&#x22;login@student.42.fr\&#x22; />\n</Field>&#x22;">
  <div className="w-full max-w-[20rem]">
    <Field label="Email" description="We'll never share it.">
      <Input placeholder="login@student.42.fr" />
    </Field>
  </div>
</Preview>

## Required [#required]

`required` sets the semantic flag (forwarded to the control) and renders a red asterisk in the label — the two always travel together.

<Preview align="block" code="&#x22;<Field label=\&#x22;Login\&#x22; required>\n  <Input placeholder=\&#x22;Pick your intra login\&#x22; />\n</Field>&#x22;">
  <div className="w-full max-w-[20rem]">
    <Field label="Login">
      <Input placeholder="Pick your intra login" />
    </Field>
  </div>
</Preview>

## Error [#error]

Passing `error` flips the field to **invalid**: the message renders below the control, is associated via `aria-describedby`, and any field-aware control inside turns red automatically — you don't set `invalid` on the control yourself. `error` renders independently of `description` — set both together and both stay visible, the description above the control and the error below it.

Set `testId` on the `Field` and it also derives `` `${testId}-error` `` on the rendered error text and `` `${testId}-description` `` on the rendered helper text — each only once that text is actually shown. See the [Testing](/docs/testing) guide.

<Preview align="block" code="&#x22;<Field label=\&#x22;Email\&#x22; error=\&#x22;Enter a valid email address\&#x22;>\n  <Input defaultValue=\&#x22;not-an-email\&#x22; />\n</Field>&#x22;">
  <div className="w-full max-w-[20rem]">
    <Field label="Email" error="Enter a valid email address">
      <Input defaultValue="not-an-email" />
    </Field>
  </div>
</Preview>

Both `description` and `error` can be set at once — they occupy independent slots on either side of the control:

<Preview align="block" code="&#x22;<Field\n  label=\&#x22;Email\&#x22;\n  description=\&#x22;We'll never share it.\&#x22;\n  error=\&#x22;Enter a valid email address\&#x22;\n>\n  <Input defaultValue=\&#x22;not-an-email\&#x22; />\n</Field>&#x22;">
  <div className="w-full max-w-[20rem]">
    <Field label="Email" description="We'll never share it." error="Enter a valid email address">
      <Input defaultValue="not-an-email" />
    </Field>
  </div>
</Preview>

## Sizes [#sizes]

`size` scales the field's own typography. Set the control's `size` to match — the family components forward a single `size` to both for you.

<Preview align="block" code="&#x22;<Field size=\&#x22;sm\&#x22; label=\&#x22;Small\&#x22;><Input size=\&#x22;sm\&#x22; /></Field>\n<Field size=\&#x22;md\&#x22; label=\&#x22;Medium\&#x22;><Input size=\&#x22;md\&#x22; /></Field>\n<Field size=\&#x22;lg\&#x22; label=\&#x22;Large\&#x22;><Input size=\&#x22;lg\&#x22; /></Field>&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-5">
    <Field size="sm" label="Small">
      <Input size="sm" placeholder="sm" />
    </Field>

    <Field size="md" label="Medium">
      <Input size="md" placeholder="md" />
    </Field>

    <Field size="lg" label="Large">
      <Input size="lg" placeholder="lg" />
    </Field>
  </div>
</Preview>

## Works with any field-aware control [#works-with-any-field-aware-control]

Because the wiring rides on Ark's `Field` context, the same `Field` wraps every control in the family — `Select`, `Combobox`, `MultiSelect`, `TagsInput`, … — and each picks up the label, description, error, and state without extra props.

```tsx
<Field label="Framework" description="Pick your stack" required>
  <Select data={['React', 'Vue', 'Svelte']} placeholder="Pick one" />
</Field>

<Field label="Tags" error="Add at least one">
  <TagsInput placeholder="Type and press Enter" />
</Field>
```

## API [#api]

<TypeTable
  type="{
  label: {
    type: 'ReactNode',
    description: 'Accessible name, rendered as a <label> bound to the control.',
  },
  description: {
    type: 'ReactNode',
    description: 'Helper text between the label and the control.',
  },
  error: {
    type: 'ReactNode',
    description: 'Error message below the control, shown independently of description. Its presence sets invalid.',
  },
  required: {
    type: 'boolean',
    default: 'false',
    description: 'Semantic required flag (forwarded to the control) + red asterisk.',
  },
  invalid: {
    type: 'boolean',
    description: 'Force the invalid state. Defaults to true when error is set.',
  },
  disabled: {
    type: 'boolean',
    default: 'false',
    description: 'Disables the field and the control nested inside.',
  },
  readOnly: {
    type: 'boolean',
    default: 'false',
    description: 'Marks the field read-only; propagates to the control.',
  },
  size: {
    type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;',
    default: '&#x22;md&#x22;',
    description: &#x22;Scales the field's label / description / error typography.&#x22;,
  },
  classNames: {
    type: '{ root?, label?, description?, error?, requiredIndicator? }',
    description: 'Per-part class overrides.',
  },
  composite: {
    type: 'boolean',
    default: 'false',
    description: 'The control is a composite (a radio group, a row of inputs) with no single labelable element. The label renders as plain text with no `for` and names the control through `aria-labelledby` instead.',
  },
  testId: {
    type: 'string',
    description: 'data-testid on the field root; also derives `${testId}-error` / `${testId}-description` on the error / description text once shown. See the Testing guide.',
  },
}"
/>

Every other prop (including `id`) forwards to Ark UI's `Field.Root`. `asChild` isn't supported: the field renders its label, description and error around the control, which can't live inside one element.

## Accessibility [#accessibility]

* The `<label>` is associated with the control automatically — clicking it focuses the control, and assistive tech announces the name.
* A `composite` control (the kit's `SegmentGroup` and `TimeInput`) has no single element to focus, so its label is plain text with no `for`; the control's group takes the label as its name through `aria-labelledby`.
* `description` and `error` are linked through `aria-describedby`, so both are read out with the control. `error` additionally sets `aria-invalid`.
* `required`, `disabled`, and `readOnly` propagate to field-aware controls through Ark's `Field` context — set them once on the `Field`.
* RTL: the asterisk uses logical spacing (`margin-inline-start`) and the layout is a plain column, so everything mirrors correctly under `dir="rtl"`.
