{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "field", "title": "Field", "description": "Form field layout primitives — set, group, label, description, error, and separator slots.", "dependencies": [ "@base-ui/react", "class-variance-authority", "clsx", "tailwind-merge" ], "registryDependencies": [ "https://ui.digital.nsw.gov.au/registry/r/theme.json", "https://ui.digital.nsw.gov.au/registry/r/label.json", "https://ui.digital.nsw.gov.au/registry/r/separator.json" ], "files": [ { "path": "src/components/field.tsx", "content": "'use client'\n\nimport { Field as FieldPrimitive } from '@base-ui/react/field'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport * as React from 'react'\n\nimport { Label } from '@/components/label'\nimport { Separator } from '@/components/separator'\nimport { cn } from '@/lib/utils'\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Base UI Field\n// ─────────────────────────────────────────────────────────────────────────────\n//\n// `Field`, `FieldLabel`, `FieldDescription`, and `FieldError` wrap Base UI's\n// Field primitive (@base-ui/react/field). Base UI owns the accessible wiring:\n// the label is associated with the control, and any description/error ids are\n// added to the control's `aria-describedby` automatically — no manual\n// `htmlFor` / `aria-describedby` / `aria-invalid` plumbing required. Set\n// `invalid` on `Field` (e.g. from react-hook-form) and the control receives\n// `aria-invalid` and the wrapper gets `data-invalid` for free.\n//\n// Base UI's Field parts must live inside a `Field.Root`. To keep the previous\n// behaviour where a label/description/error could also be used standalone\n// (e.g. a group-level FieldDescription directly inside a FieldSet), FieldLabel/\n// FieldDescription/FieldError opt into the Base UI part only when they detect a\n// surrounding `Field` (via `InsideFieldContext`) and otherwise render the plain\n// element they did before. Inside a Field they gain the automatic association;\n// outside one they are unchanged.\n//\n// `FieldSet` / `FieldLegend` stay native `
` / `` (already\n// accessible for grouping); `FieldGroup` / `FieldContent` / `FieldSeparator` /\n// `FieldTitle` remain layout-only primitives.\n//\n// ─────────────────────────────────────────────────────────────────────────────\n// Typography hierarchy (deliberate, do not collapse to a single size)\n// ─────────────────────────────────────────────────────────────────────────────\n//\n// FieldLabel inherits its font-size from the Label primitive (`text-base/\n// relaxed` = 16px) — the primary form text users read when scanning a form,\n// kept at the 16px ideal for body copy.\n// Every supporting element in this file uses `text-sm/relaxed` (14px). That\n// is below the 16px body-copy ideal but comfortably above the 12px floor\n// older versions used, so it still reads clearly as secondary information.\n// The 16/14 gap is intentional: it preserves a scannable visual hierarchy\n// (label > description / error / metadata) without shrinking supporting text\n// back down to the previous 12px.\n//\n// If you find yourself wanting to bump these to text-base, do it in\n// `label.tsx` so FieldLabel moves with the family. If you want to drop\n// them to text-xs, please raise that as a design decision — 12px is below\n// the NSW Government digital service standards minimum body-text size.\n// ─────────────────────────────────────────────────────────────────────────────\n\n// True for descendants of our (which renders Base UI's Field.Root).\nconst InsideFieldContext = React.createContext(false)\n\nfunction FieldSet({ className, ref, ...props }: React.ComponentProps<'fieldset'>) {\n return (\n [data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',\n className,\n )}\n {...props}\n />\n )\n}\n\nfunction FieldLegend({\n className,\n variant = 'legend',\n ref,\n ...props\n}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {\n return (\n \n )\n}\n\nfunction FieldGroup({ className, ref, ...props }: React.ComponentProps<'div'>) {\n return (\n [data-slot=…]]:gap-3` matches when FieldGroup CONTAINS a\n // checkbox/radio group child. The original `data-[slot=checkbox-group]:gap-3`\n // was self-referential (this element's slot is `field-group`) and\n // could never apply. Mirrors the rule on FieldSet above.\n 'group/field-group @container/field-group flex w-full flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 *:data-[slot=field-group]:gap-4',\n className,\n )}\n {...props}\n />\n )\n}\n\nconst fieldVariants = cva(\n // `data-invalid:` (presence) matches the `data-invalid` attribute Base UI's\n // Field.Root sets when the field is invalid (via the `invalid` prop or native\n // validation) — replacing the old hand-set `data-[invalid=true]`.\n 'group/field flex w-full gap-2 data-invalid:text-destructive',\n {\n variants: {\n orientation: {\n vertical: 'flex-col *:w-full [&>.sr-only]:w-auto',\n horizontal:\n 'flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',\n responsive:\n '@max-md/field-group:flex-col @max-md/field-group:*:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',\n },\n },\n defaultVariants: {\n orientation: 'vertical',\n },\n },\n)\n\nfunction Field({\n className,\n orientation = 'vertical',\n ref,\n children,\n ...props\n}: React.ComponentProps & VariantProps) {\n return (\n .\n role='group'\n data-slot='field'\n data-orientation={orientation}\n className={cn(fieldVariants({ orientation }), className)}\n {...props}\n >\n {children}\n \n )\n}\n\nfunction FieldContent({ className, ref, ...props }: React.ComponentProps<'div'>) {\n return (\n \n )\n}\n\nconst fieldLabelClassName = cn(\n 'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-disabled/field:opacity-50 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-2 dark:has-data-checked:bg-primary/10',\n 'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col',\n)\n\nfunction FieldLabel({ className, ref, ...props }: React.ComponentProps) {\n const insideField = React.useContext(InsideFieldContext)\n\n // Standalone (no surrounding Field): render the styled Label directly, as\n // before. Base UI's Field.Label would throw without a Field.Root ancestor.\n if (!insideField) {\n return (\n \n )\n }\n\n return (\n , so the ref is an\n // HTMLLabelElement at runtime.\n ref={ref as React.Ref}\n // Render through our styled Label so FieldLabel keeps the Label\n // primitive's typography + data-slot, while Base UI supplies the\n // automatic `htmlFor` association with the field control.\n render={