Field
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.
Preview
field · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/field
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { Field } from "@/components/ui/field"<Field label="Example measurement" hint="For example, 14">
<Field.Control name="example-measurement" inputMode="decimal" autoComplete="off" />
</Field>When to use it
Use it when
- Every input in the system, where a Field is the label guarantee.
- Any control needing a hint or error wired to it, not merely placed nearby.
- As the base for purpose-built inputs, the way ReadingInput is built.
Do not use it when
- A measurement with a unit and a plausible range.Use
reading-inputinstead. - The control is an action, not an input.Use
buttoninstead. - Several related inputs are captured in one interaction.Use
log-sheetinstead. - You are showing a value, not collecting one.Use
valueinstead. - A failed form's errors need summarising at the top.Use
alert-bannerinstead.
Anatomy
FieldThe root. Owns every id relationship and draws the invalid left rule.Controlled byvalidateOnField.LabelA real label element against the generated control id.Controlled bylabelField.OptionalityThe word required or optional, joining the accessible name.Controlled byoptionalityField.HintGuidance before the mistake, announced on focus through aria-describedby.Controlled byhintField.ControlThe product's control and the only real export. Takes aria-invalid and the target floor.Controlled bychildrenField.ErrorThe message and its CircleAlert glyph, never replacing the hint.Controlled byerror
- Field 1data-slot="field"; renders a <div>, gains data-invalid when the field is invalid
- Field.Label 1data-slot="field-label"; a native <label for> against the control id
- Field.Optionality 0..1data-slot="field-optionality"; nested inside the label, absent when optionality="none"
- Field.Hint 0..1data-slot="field-hint"; renders a <p>, its id joins aria-describedby
- Field.Error 0..1data-slot="field-error"; its id joins aria-describedby beside the hint
- Field.Control 1data-slot="field-control"; the consumer’s element, passed as children
- Field.Label 1data-slot="field-label"; a native <label for> against the control id
Examples
The error keeps the hint
error adds a sentence beside the hint rather than replacing it.
field-error-keeps-the-hint · base base · style base-lyraOpen under the product themeMarking the exception
One optional field among three needed ones, marked by optionality="optional".
See Required and optional.
field-marking-the-exception · base base · style base-lyraOpen under the product themeA control opsinjs does not ship
render on Field.Control puts the whole wiring onto a textarea and a
select.
field-with-another-control · base base · style base-lyraOpen under the product themeValidating 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.
field-validating-on-submit · base base · style base-lyraOpen under the product themeContent 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.
Do
"Date of birth" over three labelled fields, error "Enter a date in the past".
Don’t
"DOB" with "Invalid input."* An abbreviation, an unexplained symbol, no fix.
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.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves focus to the control | Only the control is focusable. |
| Shift+Tab | Moves focus back out of the control | Reverse order matches visual order. |
| Enter | Asks the control to check itself, and submits an enclosing form | Focus does not move and nothing is announced. |
| Escape | Nothing | Not a popup. The key reaches a dialog. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-slot | Root, label and control always; optionality, hint and error only when that part renders; Field.Form where mounted. | field, field-label, field-optionality, field-hint, field-error, field-control, field-form |
| data-invalid | On the root and control, when error is non-empty or a constraint fails. | Present with no value when invalid; absent otherwise |
API reference
Prop
Type
Generated from FieldProps in registry/bases/base/field.tsx.
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.
Prop
Type
Generated from FieldControlProps in registry/bases/base/field.tsx.
Related
- ReadingInput is a Field with a unit and a plausibility contract.
- LogSheet captures several Fields together.
- Callout is guidance about a whole form, not one control.
- AlertBanner is the summary at the top of a failed form.
Link
The control that takes the reader somewhere else, and the one place the system decides how a navigation link looks, so a banner's action and a card's match.
IconButton
A button whose only visible content is an icon, so it always requires an accessible name you supply. Prefer a labelled button wherever there is room for a word.