---
title: "Field"
description: "The wrapper that gives every input a visible label, a hint, an error message and the wiring between them. It is why no control here can ship without a name."
url: "https://opsinjs.pensievelabs.org/components/field"
source: "https://opsinjs.pensievelabs.org/components/field.md"
section: "Components"
status: "shipped"
kind: "component"
category: "actions-and-forms"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["form field", "label", "input wrapper", "error message", "hint", "form control", "validation message"]
usedIn: ["ask-users-for/contact-details", "ask-users-for/date-of-birth", "ask-users-for/ethnicity", "ask-users-for/height-and-weight", "ask-users-for/medications", "ask-users-for/name", "ask-users-for/sex-and-gender", "ask-users-for/symptoms", "consent-before-collection", "daily-log-entry", "daily-log-screen", "daily-logging", "diabetes-medicines-app", "forms/autocomplete-and-input-types", "forms/error-summaries", "forms/question-pages", "forms/required-and-optional", "forms/units-and-numeric-entry", "forms/validation-timing", "onboarding-and-first-run", "onboarding-screen"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="field"
  status="shipped"
  questions="[
  &#x22;Field ships required and optional with no prop to translate them, and whether a prop or a locale object fixes that is open.&#x22;,
  &#x22;Field mounts no live region, so an error is silent until focus reaches it. Whether Field exposes its generated ids to an error summary is open.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="field" />

## Installation [#installation]

<ComponentInstall name="field" unbuilt="false" importPath="@/components/ui/field" dependencies="[&#x22;@base-ui/react&#x22;, &#x22;lucide-react&#x22;]" />

## Usage [#usage]

```tsx
import { Field } from "@/components/ui/field"
```

```tsx
<Field label="Example measurement" hint="For example, 14">
  <Field.Control name="example-measurement" inputMode="decimal" autoComplete="off" />
</Field>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;Every input in the system, where a Field is the label guarantee.&#x22;,
  &#x22;Any control needing a hint or error wired to it, not merely placed nearby.&#x22;,
  &#x22;As the base for purpose-built inputs, the way ReadingInput is built.&#x22;,
]"
  avoid="[
  { case: &#x22;A measurement with a unit and a plausible range.&#x22;, instead: &#x22;reading-input&#x22; },
  { case: &#x22;The control is an action, not an input.&#x22;, instead: &#x22;button&#x22; },
  { case: &#x22;Several related inputs are captured in one interaction.&#x22;, instead: &#x22;log-sheet&#x22; },
  { case: &#x22;You are showing a value, not collecting one.&#x22;, instead: &#x22;value&#x22; },
  { case: &#x22;A failed form's errors need summarising at the top.&#x22;, instead: &#x22;alert-banner&#x22; },
]"
/>

## Anatomy [#anatomy]

<Anatomy
  name="field"
  parts="[
  {
    name: &#x22;Field&#x22;,
    describes: &#x22;The root. Owns every id relationship and draws the invalid left rule.&#x22;,
    prop: &#x22;validateOn&#x22;,
  },
  {
    name: &#x22;Field.Label&#x22;,
    describes: &#x22;A real label element against the generated control id.&#x22;,
    prop: &#x22;label&#x22;,
  },
  {
    name: &#x22;Field.Optionality&#x22;,
    describes: &#x22;The word required or optional, joining the accessible name.&#x22;,
    prop: &#x22;optionality&#x22;,
  },
  {
    name: &#x22;Field.Hint&#x22;,
    describes: &#x22;Guidance before the mistake, announced on focus through aria-describedby.&#x22;,
    prop: &#x22;hint&#x22;,
  },
  {
    name: &#x22;Field.Control&#x22;,
    describes: &#x22;The product's control and the only real export. Takes aria-invalid and the target floor.&#x22;,
    prop: &#x22;children&#x22;,
  },
  {
    name: &#x22;Field.Error&#x22;,
    describes: &#x22;The message and its CircleAlert glyph, never replacing the hint.&#x22;,
    prop: &#x22;error&#x22;,
  },
]"
/>

<CompositionTree
  name="field"
  tree="[
  {
    part: &#x22;Field&#x22;,
    cardinality: &#x22;1&#x22;,
    note: 'data-slot=&#x22;field&#x22;; renders a <div>, gains data-invalid when the field is invalid',
    children: [
      {
        part: &#x22;Field.Label&#x22;,
        cardinality: &#x22;1&#x22;,
        note: 'data-slot=&#x22;field-label&#x22;; a native <label for> against the control id',
        children: [
          {
            part: &#x22;Field.Optionality&#x22;,
            cardinality: &#x22;0..1&#x22;,
            note: 'data-slot=&#x22;field-optionality&#x22;; nested inside the label, absent when optionality=&#x22;none&#x22;',
          },
        ],
      },
      {
        part: &#x22;Field.Hint&#x22;,
        cardinality: &#x22;0..1&#x22;,
        note: 'data-slot=&#x22;field-hint&#x22;; renders a <p>, its id joins aria-describedby',
      },
      {
        part: &#x22;Field.Error&#x22;,
        cardinality: &#x22;0..1&#x22;,
        note: 'data-slot=&#x22;field-error&#x22;; its id joins aria-describedby beside the hint',
      },
      {
        part: &#x22;Field.Control&#x22;,
        cardinality: &#x22;1&#x22;,
        note: 'data-slot=&#x22;field-control&#x22;; the consumer’s element, passed as children',
      },
    ],
  },
]"
/>

## Examples [#examples]

### The error keeps the hint [#the-error-keeps-the-hint]

`error` adds a sentence beside the hint rather than replacing it.

<ComponentPreview name="field-error-keeps-the-hint" kind="example" align="start" />

### Marking the exception [#marking-the-exception]

One optional field among three needed ones, marked by `optionality="optional"`.
See [Required and optional](../patterns/forms/required-and-optional.mdx).

<ComponentPreview name="field-marking-the-exception" kind="example" align="start" />

### A control opsinjs does not ship [#a-control-opsinjs-does-not-ship]

`render` on `Field.Control` puts the whole wiring onto a `textarea` and a
`select`.

<ComponentPreview name="field-with-another-control" kind="example" align="start" />

### Validating on submit [#validating-on-submit]

The default `validateOn` fires on submit only inside `Field.Form`, where an
empty field on Save shows the browser's own message. Pass `error` instead.

<ComponentPreview name="field-validating-on-submit" kind="example" align="start" />

## Content guidelines [#content-guidelines]

Labels are nouns in sentence case with no colon. Hints show the shape of a valid
answer, never a number a reader could read as a result. Errors say what to fix,
without blame.

<DoDont>
  <DoDont.Do>
    **"Date of birth" over three labelled fields, error "Enter a date in the past".**
  </DoDont.Do>

  <DoDont.Dont>
    **"DOB*" with "Invalid input."*\* An abbreviation, an unexplained symbol, no fix.
  </DoDont.Dont>
</DoDont>

## Accessibility [#accessibility]

Audited against WCAG 2.2 AA in a source pass and a rendered pass on
`/view/base/base-lyra/component/field`. The audit is author-run, not an
independent review, and clinical review is pending. The rendered pass was clean:
no axe violations, targets met `--opsin-target-minimum`, focus visible, no
reflow at 320px.

**Fixed here.** The error now carries a live region, so it is announced on
submit while focus is on Save, where it was silent before.

**From source.** A native `label` against a generated id; `aria-describedby`
survives unmount; `aria-invalid` sits on the control; the invalid state is a
glyph and a left rule, never colour. `pnpm run check:a11y` guards role tokens
and non-`px` sizes.

**Limits.** The required and optional word is English only and untranslatable by
prop. The focus ring lives in `product.css`, so a `shadcn add` copy without it
shows no ring until you add one. The invalid shadow drops
under forced colours and print, where the glyph, words and rule still carry the
state.

<KeyboardTable
  name="field"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Moves focus to the control&#x22;,
    notes: &#x22;Only the control is focusable.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Moves focus back out of the control&#x22;,
    notes: &#x22;Reverse order matches visual order.&#x22;,
  },
  {
    keys: &#x22;Enter&#x22;,
    action: &#x22;Asks the control to check itself, and submits an enclosing form&#x22;,
    notes: &#x22;Focus does not move and nothing is announced.&#x22;,
  },
  {
    keys: &#x22;Escape&#x22;,
    action: &#x22;Nothing&#x22;,
    notes: &#x22;Not a popup. The key reaches a dialog.&#x22;,
  },
]"
/>

<ContrastReport component="field" />

## Data attributes [#data-attributes]

<DataAttributesTable
  name="field"
  rows="[
  {
    attribute: &#x22;data-slot&#x22;,
    condition: &#x22;Root, label and control always; optionality, hint and error only when that part renders; Field.Form where mounted.&#x22;,
    value: &#x22;field, field-label, field-optionality, field-hint, field-error, field-control, field-form&#x22;,
  },
  {
    attribute: &#x22;data-invalid&#x22;,
    condition: &#x22;On the root and control, when error is non-empty or a constraint fails.&#x22;,
    value: &#x22;Present with no value when invalid; absent otherwise&#x22;,
  },
]"
/>

## API reference [#api-reference]

<PropsTable name="FieldProps" />

`label` is required and there is no `hideLabel`. An empty `label` or `error` is
refused with a development warning. Without a `Field.Form` ancestor the default
`validateOn` checks on Enter alone.

<PropsTable name="FieldControlProps" />

## Related [#related]

* [ReadingInput](./reading-input.mdx) is a Field with a unit and a plausibility contract.
* [LogSheet](./log-sheet.mdx) captures several Fields together.
* [Callout](./callout.mdx) is guidance about a whole form, not one control.
* [AlertBanner](./alert-banner.mdx) is the summary at the top of a failed form.
