Field

canon

Wrapper that stacks label, control, hint and error message.

acrm MR !4085 (28 Sep 2026, "fix(ui): chyba pod DS Field se čtečce provází sama") added a new export, `useFieldDescribedBy`: `<Field>` now gives the rendered hint/error span an automatic `id` (`useId()`, error takes precedence over hint) and provides that id through a context. Any control rendered as a child of `<Field>`/`<DrawerField>` can call `useFieldDescribedBy()` and fall back to it for `aria-describedby` unless the caller passed an explicit `describedBy` — `masked-input` and `phone-input` both do this now. This does not add `htmlFor`/label association (the pre-existing known limit in `field.a11y.md` — no automatic label–control link), and it only helps a control that explicitly opts into the hook; a plain `<input>` still needs it wired by hand.

Used in 29 files · 15 areas · 214× — components/accounting, components/callwrapup, components/clen-tymu, components/datamart, components/mail, components/person …

Keyboard

  • Field adds no tab stop; the control inside keeps its own keyboard behaviour.
  • labelExtra that is interactive (a help icon button) becomes a tab stop before the control — keep it a real <button> so the foundation focus ring applies.

ARIA

  • The label is a <label> element but <Field> sets no htmlFor and the control is a sibling, not a child — the association is NOT automatic. Give the control an accessible name: aria-label, or set id on the control and aria-labelledby on it pointing at the label (add an id to the label via labelExtra-free markup or your own wrapper).
  • aria-describedby is automatic as of acrm MR !4085 (fix(ui): chyba pod DS Field se čtečce provází sama, AD 28 Sep 2026, nález from M1's "Nová firma" review): <Field> gives the rendered hint/error span an automatic id (useId()) — error takes precedence over hint, same as the visual rule — and exposes that id through a context (export const useFieldDescribedBy = () => useContext(FieldDescribedBy), in Field.tsx, re-exported from @acrm/ui). A control rendered as <Field>'s child reads useFieldDescribedBy() and falls back to it for its own aria-describedby unless the caller passed an explicit one — MaskedInput and PhoneInput both do this (aria-describedby={describedBy ?? fieldDescribedBy}). Any other control placed inside <Field>/<DrawerField> still needs to opt in the same way (import { useFieldDescribedBy } from '@acrm/ui') — the context exists, but nothing forces a consumer to read it; a bespoke <input> that ignores it is back to the pre-!4085 gap below.
  • The error span has no role="alert" / aria-live; a validation message that appears after submit is not announced unless the screen moves focus to the field or adds a live region.

Focus

  • Nothing to focus in the wrapper itself; focus styling belongs to the control (Input ring, foundation outline on buttons).
  • Clicking the label does not move focus to the control (no htmlFor).

Contrast

Computed from packages/ui/src/tokens/tokens.css:

  • Label --fg-secondary #7d8184 on --surface-100 #ffffff = 3.93:1 (light) — below AA 4.5:1 for 12px text; #9a9ca1 on #1b1c1f = 6.20:1 (dark).
  • Hint --fg-muted #98999c on white = 2.85:1 (light); #717479 on #1b1c1f = 3.63:1 (dark) — below AA.
  • Error --critical-ink #cb2a2f on white = 5.37:1 (light); #ff6166 on #1b1c1f = 5.80:1 (dark) — passes AA.

Known limits

  • No label–control association out of the box (no htmlFor / id props on <Field>).
  • Hint/error now carry an automatic id and are reachable via useFieldDescribedBy() (acrm MR !4085), but nothing wires them by itself: only controls that explicitly call the hook (MaskedInput, PhoneInput) pick it up. A plain <input> or a third-party control dropped into <Field> still needs aria-describedby={useFieldDescribedBy()} added by hand.
  • Neither the hint nor the error span has role="alert" / aria-live.
  • Label (3.93:1) and hint (2.85:1) in light theme are below AA for 12px text.

Source: docs/text-field/field.a11y.md · GitLab