{
  "name": "Field",
  "type": "component",
  "description": "A form field: label, control, hint and validation message, wired together by Base UI's Field. Composed from Field > FieldLabel > FieldControl > FieldDescription > FieldError, with FieldGroup, FieldSet, FieldLegend, FieldTitle, FieldContent and FieldSeparator for form-level structure.",
  "export": "@gtmi/strix-react/atoms/field",
  "path": "src/atoms/field/field.tsx",
  "figma": null,
  "figmaAbsent": "shadcn form-field wiring. Figma draws labels and inputs but no field-group abstraction.",
  "props": [
    {
      "name": "invalid",
      "type": "boolean",
      "description": "Sets aria-invalid and data-invalid, and shows FieldError — which is a deliberate divergence from Base UI, where it does not. See notes."
    },
    {
      "name": "disabled",
      "type": "boolean",
      "description": "Disables the control, so :disabled utilities fire."
    }
  ],
  "parts": [
    {
      "name": "Field",
      "description": "The root. Owns the id/aria-describedby/aria-invalid wiring."
    },
    {
      "name": "FieldLabel",
      "description": "Base UI's label, associated automatically — no htmlFor is written by hand and none can go stale. Visual treatment is Label's."
    },
    {
      "name": "FieldControl",
      "description": "The control. Renders a bare input by default; takes render={<Input />} / render={<Textarea />} so styling stays in the component being rendered."
    },
    {
      "name": "FieldDescription",
      "description": "A hint, linked into aria-describedby. Uses HelpText's default ink."
    },
    {
      "name": "FieldError",
      "description": "A validation message. Shows when the Field root is `invalid`, so an external validation library needs no `match` at all. `match` still overrides: a ValidityState key such as \"valueMissing\" for the native path, or `false` to suppress."
    },
    {
      "name": "FieldGroup",
      "description": "A stack of fields at form rhythm."
    },
    {
      "name": "FieldSet",
      "description": "A named group. Base UI's Fieldset associates the legend, so the group is announced rather than merely boxed."
    },
    {
      "name": "FieldLegend",
      "description": "The group's accessible name."
    },
    {
      "name": "FieldContent",
      "description": "A row that puts a control beside its label — a switch or checkbox."
    },
    {
      "name": "FieldTitle",
      "description": "A section heading inside a long form that is not a legend."
    },
    {
      "name": "FieldSeparator",
      "description": "A rule between sections. aria-hidden, because it is decoration."
    }
  ],
  "ai_tags": ["form", "field", "label", "validation", "accessibility"],
  "since": "0.2.0",
  "tokens_used": [
    "--color-text-primary",
    "--color-text-secondary",
    "--color-text-error",
    "--color-border-neutral-soft"
  ],
  "notes": "WHY THIS EXISTS. Label, HelpText and Input all existed and every consumer wired id, aria-describedby and aria-invalid between them by hand — work that gets silently skipped, leaving a field whose hint no screen reader reads. Base UI's Field owns that wiring.\n\nDIVERGES FROM BASE UI ON ONE POINT, DELIBERATELY: `<Field invalid>` shows FieldError. Base UI renders Field.Error only when `match` is given — `invalid` sets aria-invalid on the control and data-invalid on the root, label and control, so every visual treatment keyed to invalid fires, while the message stays absent because the primitive gates it on the control's own ValidityState. The result was a field that turns red and says nothing, and every consumer driving validity from Formik or React Hook Form hit it and had to learn to pass `match` themselves. So FieldError defaults `match` to the root's `invalid`, read off a context this wrapper provides, and `match ?? invalid` (not `||`) keeps `match={false}` working as a suppression. Base UI's own behaviour is still pinned in base-ui-field.characterisation.test.tsx; the divergence is asserted in field.test.tsx. `match` is NOT called forceShow, which was the first guess.\n\nCOMPOSES THE EXISTING COMPONENTS rather than restating them: FieldLabel renders Strix's own Label through BaseField.Label's `render` seam, and FieldDescription uses helpTextStateColour, exported from help-text for this rather than duplicated.\n\nIT USED TO DRIFT WHILE CLAIMING IT DID NOT. FieldLabel applied labelStyles() — the three-class box, not the type — and then wrote its own ramp over the top: text-size-body-small font-body-small-medium, 14px at weight 500, where a standalone Label is body-m-semibold at 16/600. It also dropped font-sans entirely, so the label fell through to inherited family. Two label treatments coexisted on one form. Every assertion that existed checked one shared token, which both sides had, so all of them passed; the test now compares the whole class list. Figma says 16/24 semibold twice — .label (22:3960) and .input-dropdown both draw the label under Paragraph/medium-semibold, and input-dropdown's binding calls \"Medium\" here an error explicitly. 14/Medium is Paragraph/small-medium, which .input-text puts on the value INSIDE the field; Dev Mode names that layer [FieldLabel], which is the likeliest route by which it reached the label above the field.\n\nNOT VERIFIED IN JSDOM: the native validation path. jsdom does report validity.valueMissing on the control, but Base UI does not surface the message on blur or submit there, so the test asserts the mechanism — required is set, jsdom agrees the control is invalid, match reaches the primitive — and says plainly that the rendered result needs a browser. Writing a test that appeared to cover it would pass for the wrong reason.\n\nSOURCE: Base UI Field and Fieldset, with the part names from shadcn/ui's Base UI flavour. There is no Figma atom for a field wrapper; .input-text's label + description + error rows are what implied it."
}
