# Field

A form field with a label above the control and a hint or an error below it.

```tsx
import { Button, Field, Input, Textarea } from 'ferry-ui'

export default function FieldHero() {
  return (
    <form className="mx-auto flex w-full max-w-sm flex-col gap-5" onSubmit={(event) => event.preventDefault()}>
      <Field label="Project name">
        <Input placeholder="Billing portal" />
      </Field>
      <Field label="Billing email" hint="The app sends the invoices to this address.">
        <Input type="email" placeholder="maya@example.com" />
      </Field>
      <Field label="Description" optional>
        <Textarea placeholder="What is this project about?" />
      </Field>
      <Button type="submit" variant="primary" size="md" className="self-end">
        Create project
      </Button>
    </form>
  )
}
```

## Usage guidelines

- **For a label above the control.** Use `Field` in a dialog, a popover, a sign-in form or a form that stands alone.
- **A settings row is not a field.** For a label on the left of the control, use `FormRow` of [Form Card](/docs/components/form-card).
- **A checkbox has its label on the same line.** For a [Checkbox](/docs/components/checkbox) or a [Switch](/docs/components/switch), use [Label](/docs/components/label).
- **Keep the label short.** A label has 1 to 4 words. Put the help text in `hint`.

## Anatomy

Import the component. A field has one part. Its child is the control.

```tsx title="Anatomy"

<Field label="" hint="" error="">
  <Input />
</Field>
```

`Field` sets `id`, `aria-invalid` and `aria-describedby` on the control for you.

## Examples

### Error

Pass the message to `error`. The message shows below the control and the control gets its error border. The error takes the place of the hint.

```tsx
import * as React from 'react'
import { Field, Input } from 'ferry-ui'

const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

export default function FieldError() {
  const [email, setEmail] = React.useState('maya@example')
  const error = email !== '' && !EMAIL.test(email) ? 'Enter a valid email address.' : undefined

  return (
    <Field label="Work email" hint="The app sends the sign-in link here." error={error} className="mx-auto w-full max-w-sm">
      <Input type="email" value={email} onChange={(event) => setEmail(event.target.value)} />
    </Field>
  )
}
```

### Error and hint

If the hint helps the user to correct the value, set `errorReplacesHint` to `false`. The field then shows the hint and the error.

```tsx
import * as React from 'react'
import { Field, Input } from 'ferry-ui'

export default function FieldErrorAndHint() {
  const [seats, setSeats] = React.useState('80')
  const count = Number(seats)
  const valid = Number.isInteger(count) && count >= 1 && count <= 50

  return (
    <Field
      label="Seats"
      hint="Between 1 and 50. Each seat is on the invoice."
      error={valid ? undefined : 'Enter a whole number between 1 and 50.'}
      errorReplacesHint={false}
      className="mx-auto w-full max-w-sm"
    >
      <Input type="number" value={seats} onChange={(event) => setSeats(event.target.value)} className="w-24 tabular" />
    </Field>
  )
}
```

### Optional mark

The `optional` prop adds "(optional)" to the label. Pass a text to replace these words. Use the mark when most fields of the form are required.

```tsx
import { Field, Input } from 'ferry-ui'

export default function FieldOptional() {
  return (
    <div className="mx-auto flex w-full max-w-sm flex-col gap-5">
      <Field label="Company" optional hint="The name shows on your invoices.">
        <Input placeholder="Acme" />
      </Field>
      <Field label="Tax number" optional="(if you have one)">
        <Input mono />
      </Field>
    </div>
  )
}
```

### Sizes

The `size` prop sets the text size of the label and of the hint.

```tsx
import { Field, Input } from 'ferry-ui'

export default function FieldSizes() {
  return (
    <div className="mx-auto flex w-full max-w-sm flex-col gap-6">
      <Field size="md" label="Medium" hint="For a form in a page.">
        <Input placeholder="Billing portal" />
      </Field>
      <Field size="sm" label="Small" hint="For a form in a dialog or a popover.">
        <Input placeholder="Billing portal" />
      </Field>
    </div>
  )
}
```

| Size | Use |
| --- | --- |
| `md` (default) | A form in a page, a sign-in form. |
| `sm` | A form in a dialog or a popover. |

### Label variants

The `labelVariant` prop sets the look of the label.

```tsx
import { CopyField, Field, Input } from 'ferry-ui'

export default function FieldLabelVariants() {
  return (
    <div className="mx-auto flex w-full max-w-sm flex-col gap-6">
      <Field label="Project name" hint="The default label.">
        <Input placeholder="Billing portal" />
      </Field>
      <Field labelVariant="subtle" label="Email" hint="The subtle label.">
        <Input type="email" placeholder="maya@example.com" />
      </Field>
      <Field labelVariant="mono" size="sm" label="API base URL" hint="The mono label, above a value to copy.">
        <CopyField size="sm" value="https://api.example.com/v1" what="API base URL" />
      </Field>
    </div>
  )
}
```

| Variant | Use |
| --- | --- |
| `default` | The standard label, with a medium weight. |
| `subtle` | A sign-in form, a secondary field. |
| `mono` | A read-only value in a popover or a panel. Use it with `size="sm"`. |

### Composite control

A [Select](/docs/components/select) has many parts. Pass a function as the child. Spread its `control` argument on the part that takes the focus.

```tsx
import { Field, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'ferry-ui'

export default function FieldRenderFunction() {
  return (
    <Field label="Currency" hint="The app uses it for new invoices." className="mx-auto w-full max-w-sm">
      {(control) => (
        <Select defaultValue="eur">
          {/* The trigger takes the focus: it gets the id and the ARIA attributes. */}
          <SelectTrigger {...control} className="w-full">
            <SelectValue />
          </SelectTrigger>
          <SelectContent position="popper">
            <SelectItem value="eur">Euro (EUR)</SelectItem>
            <SelectItem value="usd">US dollar (USD)</SelectItem>
            <SelectItem value="gbp">Pound sterling (GBP)</SelectItem>
          </SelectContent>
        </Select>
      )}
    </Field>
  )
}
```

### Control with a button

Use the same function to put a button next to an [Input](/docs/components/input). Only the input gets the `control` props.

```tsx
import { Button, Field, Input } from 'ferry-ui'
import { Plus } from 'lucide-react'

export default function FieldInputWithButton() {
  return (
    <Field label="Invite by email" hint="The member gets an email with a link." className="mx-auto w-full max-w-sm">
      {(control) => (
        <div className="flex gap-2">
          <Input {...control} type="email" placeholder="maya@example.com" />
          <Button icon={<Plus />} size="md" className="shrink-0">
            Add
          </Button>
        </div>
      )}
    </Field>
  )
}
```

### Group of controls

A [Radio Group](/docs/components/radio-group) has no single control for the label. Set `labelAs` to `span`. The group then gets `aria-labelledby`.

```tsx
import { Field, Label, RadioGroup, RadioGroupItem } from 'ferry-ui'

export default function FieldGroup() {
  return (
    <Field label="Billing period" labelAs="span" hint="You can change the period later." className="mx-auto w-full max-w-sm">
      <RadioGroup defaultValue="monthly">
        <Label className="font-normal">
          <RadioGroupItem value="monthly" /> Monthly
        </Label>
        <Label className="font-normal">
          <RadioGroupItem value="yearly" /> Yearly, two months free
        </Label>
      </RadioGroup>
    </Field>
  )
}
```

## Accessibility

- A click on the label moves the focus to the control.
- The error has `role="alert"`. Screen readers read the message when it shows.
- `aria-describedby` of the control points to the hint and to the error.

## API reference

`Field` also accepts each attribute of the `<div>` element. They go to the root element.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `ReactNode` |  | Field name shown above the control. Keep it short (1–4 words); put guidance in `hint`. |
| `children` (required) | `ReactElement \| ((control: FieldControlProps, meta: FieldRenderMeta) => ReactNode)` |  | The control. Either a single element (Input, Textarea, RadioGroup…): Field injects `id`, `aria-invalid` and `aria-describedby` into it, merging any `aria-describedby` already set on it. Or a render function `(control, { labelId }) => node` for composite controls: spread `control` onto the focusable element (a `SelectTrigger`, the input of an input + button row). |
| `id` | `string` |  | Id of the control. Defaults to the child control's own `id`, else a generated one. The label gets `<id>-label`, the hint `<id>-hint` and the error `<id>-error`. |
| `hint` | `ReactNode` |  | Help text under the control (format rules, what the value is used for). |
| `error` | `ReactNode` |  | Validation message under the control, rendered as `<p role="alert">`. While set, the control gets `aria-invalid` and its `aria-describedby` points at the error line. |
| `optional` | `ReactNode` | `false` | Appends a muted marker to the label: `true` shows "(optional)", a string or node replaces that text (e.g. `"(facultatif)"` in a French UI). Use it when most fields of the form are required; do not mark every field of a mostly optional form. |
| `errorReplacesHint` | `boolean` | `true` | `true` (default): the error takes the hint's place while shown, so the field keeps its height. `false`: both lines stay visible (hint first) and `aria-describedby` lists both; use it when the hint remains useful to fix the error (syntax help, allowed range). |
| `size` | `"sm" \| "md"` | `md` | Type scale. `md` (default, page and auth forms): 14px label, 13px hint/error, 8px gaps. `sm` (dialogs, popovers, dense multi-column forms): 13px label, 12.5px hint/error, 6px gaps. |
| `labelVariant` | `"default" \| "mono" \| "subtle"` | `default` | Label style. `default`: medium weight, full foreground. `subtle`: normal weight, lighter color, for sign-in forms and secondary inputs (a confirmation box). `mono`: the small UPPERCASE monospace caption (with a 12px hint), for read-only values in popovers and panels; pair it with `size="sm"`. Do not use `mono` for editable form fields: it reads as a caption, not a prompt. |
| `labelAs` | `"label" \| "span"` | `label` | Label element. `label` (default) is tied to the control with `for`, so clicking it focuses the control. Use `span` when there is no single focus target (a radio group, toggle group, card picker, or a read-only block such as a code snippet): the span gets the id `<id>-label` and the control receives `aria-labelledby`. |

### Render function

The function gets two arguments: `control`, then an object with `labelId`.

| Key of `control` | Type | Role |
| --- | --- | --- |
| `id` | `string` | The id of the control. |
| `aria-invalid` | `true` | Set while the field shows an error. |
| `aria-describedby` | `string` | The ids of the hint and of the error. |
| `aria-labelledby` | `string` | The id of the label. Set only with `labelAs="span"`. |
