# Field Frame

> The frame every field wears: a label above, the control, and a hint or an error under it, wired up for screen readers. Also exports FieldButton, the field-shaped trigger for lists and pickers.

- Name: `field-frame`
- Status: beta
- Categories: forms
- File: the install writes `field-frame.tsx` to your `ui` alias, `@/components/ui` by default.
- Other registry items: `@arbokui/motion`, `@arbokui/input`. The install adds them.
- Gallery: https://arbokui.com/components/field-frame
- Registry item: https://arbokui.com/r/field-frame.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/field-frame
   ```

## Use it for

- Frame any field with it, so every form reads, sizes, and announces the same way.
- Give the frame the control's id. The label gets \<id>-label and the hint or error gets \<id>-note.
- Spread fieldIds(id, { label, hint, error, control }) on the control. It sets aria-describedby, aria-invalid, and, for a button or a group, aria-labelledby. A button cannot take aria-invalid, so it gets data-invalid, which draws the red edge.
- fieldIds lives in a client file, so call it from a client component. In a server component, write the same ids by hand: aria-describedby="\<id>-note", and aria-labelledby="\<id>-label \<id>" for a button.
- control="input" for a native input or textarea. The label is a real \<label> that points at it.
- control="button" for a trigger whose text is its value, like a picker. It reads as the label and the value: "Room, Boardroom". Pressing the label focuses the button and does not open it.
- control="custom" for a group or an editor. The label names it. Pass onLabel to say what pressing the label focuses.
- Use FieldButton for anything that opens a list or a picker from a field. It has the field's box and states, and a chevron that turns over while open.
- Put a saving status in status, and a counter like "12/80" in aside.

## Avoid it for

- A label you hide. Every field needs a name people can see.
- Putting the error only in the red edge. Pass it as error, so it is said and shown in words.
- A form section with several fields. Use a heading or a fieldset for the section, and a frame for each field.

## Props

- `id`: `string`. Required. The control's id. The label and the note take ids built from it.
- `label`: `ReactNode`. Names the control. Pressing it focuses the control.
- `hint`: `ReactNode`. A short note under the control that describes it.
- `error`: `ReactNode`. Replaces the hint while set, in red. Announced once each time a new message appears.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. Label and note text size, and the gaps. Match the control's size.
- `control`: `"input" | "button" | "custom"`. Default `"input"`. What the frame holds, which decides how the label names it.
- `onLabel`: `() => void`. What pressing a label that is not a native label does. By default it focuses the element with the id.
- `aside`: `ReactNode`. Ends the note line, like a counter. Muted, with figures that keep their width.
- `status`: `ReactNode`. Ends the label line, like a saving status. Kept to the label's height.
- `disabled`: `boolean`. Default `false`. Dims the label to match a disabled control.
- `className`: `string`. Extra classes for the frame. Later classes win.

## Accessibility

- For control="input" the label is a native \<label htmlFor>, so clicking it focuses the input and screen readers read it as the name.
- For a button or a group the label is plain text with an id, named through aria-labelledby. A native label would press the button.
- The hint or error has the id \<id>-note. fieldIds points the control's aria-describedby at it.
- The error has role="alert". It mounts fresh for each new message, so screen readers say it once when it appears.
- The error color is the danger tone's soft text, which the theme tests hold to 4.5:1. The words carry the meaning, not only the color.
- FieldButton sets aria-haspopup, aria-expanded, and aria-controls while open. A popup trigger, like PopoverTrigger, sets them itself instead.

## Examples

### A text field with a hint

```tsx
<FieldFrame id="shirt" label="Shirt size" hint="Shirts run small.">
  <Input id="shirt" aria-describedby="shirt-note" />
</FieldFrame>
```

### An error in place of the hint

```tsx
<FieldFrame id="handle" label="Username" hint="Letters and numbers." error="That name is taken.">
  <Input id="handle" defaultValue="admin" {...fieldIds("handle", { hint: "Letters and numbers.", error: "That name is taken." })} />
</FieldFrame>
```

### A picker trigger

```tsx
<FieldFrame id="room" label="Room" control="button">
  <FieldButton id="room" filled {...fieldIds("room", { label: "Room", control: "button" })}>
    Boardroom
  </FieldButton>
</FieldFrame>
```
