# Field

> The labeled-field scaffold — label, control, and hint/error — wired for accessibility.

URL: https://apogee.avaui.dev/docs/field
Last updated: 2026-10-15

The labeled-field scaffold — label, control, and hint/error — wired for accessibility.

Last updated October 15, 2026

```astro
<Field id="email" label="Email" hint="We'll never share it.">
<input id="email" type="email" class="w-full rounded-md border border-neutral bg-page px-3 py-2 text-sm" />
</Field>
```

## Add

```sh
npx @ava-ui/apogee add field
```

No Apogee dependencies. `Input`, `Textarea`, and `Select` render `Field` internally with a matching `id` — use `Field` standalone only for a custom control.

## Usage

```astro
---
import Field from '@/components/apogee/Field/Field.astro'
---
<Field id="name" label="Name" required>
  <input id="name" class="w-full rounded-md border border-neutral bg-page px-3 py-2 text-sm" />
</Field>
```

## Anatomy

-   **Label** — rendered when `label` is passed, `for={id}`; appends a `aria-hidden` `*` when `required` is true.
-   **Control** — the default slot. Pass your own input/textarea/select with a matching `id`, and set its `aria-describedby` to `${id}-hint` or `${id}-error` to wire the association yourself.
-   **Hint** — rendered at `${id}-hint` when `hint` is set and there’s no `error`.
-   **Error** — rendered at `${id}-error` with `role="alert"` when `error` is set (takes priority over `hint`).

## API

`@composable` — exposed to Content Editors in AVA. Any other attribute passes through to the root `<div>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | Required. Matches the control’s `id`; used to derive `${id}-hint` / `${id}-error`. |
| `label` | `string` | — | Optional label text. |
| `tooltip` | `string` | — | Help text behind an info icon after the label. The icon stays focusable when the control is disabled. |
| `hint` | `string` | — | Optional helper text. Shown with the error when both are set. |
| `error` | `string` | — | Optional error message, rendered with `role="alert"` after the hint. |
| `disabled` | `boolean` | `false` | Dims the label and marks the wrapper `aria-disabled`; disable the control too. |
| `size` | `'small' \| 'medium' \| 'large' \| 'xlarge'` | `'medium'` | Label and hint text size, matching the control’s size. Also set as `data-size` on the root. |
| `required` | `boolean` | `false` | Shows a `*` next to the label (visual only — set `required`/`aria-required` on the control itself). |
| `bare` | `boolean` | `false` | Renders only the slot, no wrapper; the parent owns label, hint and error. InputGroup uses it. |
| `hideRequiredIndicator` | `boolean` | `false` | Hides the `*` while you keep `required` on the control (a form where every field is required). |
| `class` | `string` | — | Extra classes on the root, merged last. |

## States

-   **Error** — `error` shows the message under the control in critical text; Input, Select and Textarea also set `aria-invalid`. Hint and error render together when both are set.
-   **Disabled** — the label switches to the disabled colour and the root gets `aria-disabled`. A `tooltip` help icon stays focusable.
-   **Required** — an asterisk after the label; `hideRequiredIndicator` keeps the semantics without it.

## Examples

With a hint:

```astro
<Field id="username" label="Username" hint="Letters, numbers, and dashes only.">
<input id="username" class="w-full rounded-md border border-neutral bg-page px-3 py-2 text-sm" />
</Field>
```

With an error:

```astro
<Field id="pw" label="Password" required error="Must be at least 8 characters.">
<input id="pw" type="password" class="w-full rounded-md border border-critical bg-page px-3 py-2 text-sm" />
</Field>
```

Hint and error together (the control lists both ids in `aria-describedby`):

```astro
<Field id="pw2" label="Password" hint="At least 8 characters." error="Too short.">
<input id="pw2" type="password" aria-describedby="pw2-hint pw2-error" aria-invalid="true" class="…" />
</Field>
```

A help tooltip after the label. The icon is a button outside the `<label>`, so keyboard users reach it even when the control is disabled:

```astro
<Field id="api" label="API key" tooltip="Found under Settings → Developers." hint="Rotates every 90 days.">
<input id="api" disabled class="…" />
</Field>
<Field id="all" label="Email" required hideRequiredIndicator>
<input id="all" type="email" required class="…" />
</Field>
```

Custom control (a `Field` wrapping something other than a native input) — set `aria-describedby` yourself since `Field` only renders the ids, it doesn’t wire them:

```astro
<Field id="plan" label="Plan">
  <select id="plan" aria-describedby="plan-hint" class="w-full rounded-md border border-neutral bg-page px-3 py-2 text-sm">
    <option>Free</option>
    <option>Pro</option>
  </select>
</Field>
```

## Sizes and disabled

```astro
<Field id="f-s" label="Small" size="small"><input id="f-s" class="…" /></Field>
… medium, large, xlarge …
<Field id="f-d" label="Disabled" disabled><input id="f-d" disabled class="…" /></Field>
```

## Accessibility

-   `label` is a real `<label for={id}>` — clicking it focuses the control.
-   The error message uses `role="alert"` so screen readers announce it when it appears.
-   `Field` renders the hint/error ids but doesn’t set `aria-describedby` on your control — since the control is an opaque slot, wire that attribute yourself (`Input`/`Textarea`/`Select` do this for you).
-   The `*` for `required` is `aria-hidden` (decorative) — set the real `required`/`aria-required` attribute on the control. `hideRequiredIndicator` removes only the glyph.
-   `tooltip` renders a `button` with `aria-label="More information"` beside the label, outside it, so it stays in the tab order when the control is disabled; Tooltip describes the button, not the control.

## Tokens

| Token | Used for |
| --- | --- |
| `--foreground-neutral` | Label text |
| `--foreground-neutral-faded` | Hint text |
| `--foreground-critical` | Required `*` and error text |

## Do / Don’t

-   **Do** keep the `id` on `Field` and your control in sync — it’s how hint/error ids line up.
-   **Do** use `error` instead of `hint` once a field is invalid; don’t show both.
-   **Don’t** nest more than one control per `Field` — one label should describe one input.
-   **Don’t** rely on the visual `*` alone to mark a field required — set the attribute on the control too.

## Content

-   Label is a short noun phrase (1–3 words), sentence case, no ending colon or period.
-   Hint is one short sentence explaining format or constraints, not a duplicate of the label.
-   Error names the specific problem and, where possible, the fix (“Must be at least 8 characters”), one sentence.

## Related

[

InputA labeled text field with hint/error messaging, wired for accessibility — plus a pure-CSS floating label.

](https://apogee.avaui.dev/docs/input)[

TextareaA labeled multiline input, wired the same way as Input — label, hint, and error.

](https://apogee.avaui.dev/docs/textarea)[

SelectA labeled, native <select> — full keyboard and mobile picker support for free — or a custom listbox with rich options over the same form control.

](https://apogee.avaui.dev/docs/select)[

CheckboxGroupA fieldset of Checkbox controls with one legend, hint and error.

](https://apogee.avaui.dev/docs/checkbox-group)
