SuperAPI AI Directory

shadcn-syntax-field

General & other

View repository

Updated 10-15 22:18

About this skill

shadcn ui : Field primitive (a11y form foundation, 2026)

The Field family is the accessibility foundation introduced in shadcn ui 2026. It provides the same label/description/error wiring as the older Form composition but WITHOUT coupling to react-hook-form. Every claim in this skill traces to the canonical source at apps/v4/registry/new-york-v4/ui/field.tsx in shadcn-ui/ui and to the official docs at https://ui.shadcn.com/docs/components/radix/field and https://ui.shadcn.com/docs/forms/react-hook-form.

ALWAYS read shadcn-core-architecture first if the copy-not-install paradigm is unclear. ALWAYS read shadcn-syntax-form when working on an existing codebase that already uses the older <Form> / <FormField> composition; the two layers coexist but should not be mixed inside one field tree.

Quick Reference

Minimal Field + react-hook-form Controller (canonical 2026 path)

"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"

import { Button } from "@/components/ui/button" import { Field, FieldDescription, FieldError, FieldLabel, } from "@/components/ui/field" import { Input } from "@/components/ui/input"

const formSchema = z.object({ title: z.string().min(5, "At least 5 characters.").max(32), })

type FormValues = z.infer

export function BugReportForm() { const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { title: "" }, })

function onSubmit(values: FormValues) { console.log(values) }

return ( <Controller name="title" control={form.control} render={({ field, fieldState }) => ( Bug title <Input {...field} id={field.name} aria-invalid={fieldState.invalid} placeholder="Login button not working on mobile" autoComplete="off" /> Concise summary of the bug. {fieldState.invalid && } )} /> Submit ) }


### Ten-primitive map

| Primitive | Renders | Role |
|-----------|---------|------|
| `Field` | `<div role="group" data-slot="field" data-orientation="...">` | Container per logical input. `orientation="vertical"` (default), `"horizontal"`, or `"responsive"`. `data-invalid` drives destructive colour tokens. |
| `FieldLabel` | shadcn `<Label>` with `data-slot="field-label"` | Visible label. ALWAYS set `htmlFor` to match the inner control's `id`. |
| `FieldDescription` | `<p data-slot="field-description">` | Helper text below or beside the control. |
| `FieldError` | `<div role="alert" data-slot="field-error">` | Renders error message(s). Accepts `errors={[...]}` array (Standard Schema issues) OR `children`. Returns `null` when both are empty. |
| `FieldGroup` | `<div data-slot="field-group" class="@container/field-group">` | Stacks multiple `Field` siblings. Establishes a container query (`@container/field-group`) that lets `orientation="responsive"` switch at `@md`. |
| `FieldSet` | `<fieldset data-slot="field-set">` | Semantic grouping of related controls (e.g. address block, checkbox group). |
| `FieldLegend` | `<legend data-slot="field-legend" data-variant="legend|label">` | Heading for a `FieldSet`. `variant="label"` for nested fieldsets. |
| `FieldContent` | `<div data-slot="field-content">` | Flex-column wrapper grouping a label, descriptions, and a non-input visual (used in selectable card patterns). |
| `FieldSeparator` | `<div data-slot="field-separator">` containing `<Separator>` and optional inline content | Visual divider between sibling Fields inside a `FieldGroup`. |
| `FieldTitle` | `<div data-slot="field-label">` | Title text inside a `FieldContent` (NOT a `<label>` element; use when no control needs `htmlFor`). |

NEVER add a primitive that is not in this list. The ten above are the entire export surface of `field.tsx`.

## The composition tree

ALWAYS compose primitives in this tree :

ALWAYS keep FieldError as a direct child of its Field parent. NEVER place a FieldError outside the Field it describes : the data-invalid styling on the parent Field is scoped via group/field and a sibling-out-of-tree breaks the cascade.

ALWAYS provide htmlFor on FieldLabel matching an id on the inner control. Field does NOT auto-generate ids (unlike the older FormItem); the explicit pairing is your responsibility. The canonical docs example uses htmlFor={field.name} plus id={field.name} from react-hook-form's Controller.

The data-invalid pattern

The Field wrapper exposes its invalid state via the data-invalid attribute. Internally fieldVariants (the cva object in field.tsx) declares :

"group/field flex w-full gap-3 data-[invalid=true]:text-destructive"

That single class lights up the destructive colour token on every descendant that opts in via the group/field selector. ALWAYS set data-invalid={fieldState.invalid} (RHF) or data-invalid={isInvalid} (TanStack) on Field. ALWAYS ALSO set aria-invalid={fieldState.invalid} on the inner input so assistive tech announces the error : data-invalid is presentational, aria-invalid is the accessibility signal.

NEVER omit aria-invalid on the input. The Field wrapper does NOT propagate it down (no Slot.Root involved); the input must carry the attribute itself.

Orientation

Field has a orientation prop with three values, driven by cva :

Value Layout Use when
"vertical" (default) flex-col ; label above control, full-width Standard mobile-first forms ; vast majority of cases.
"horizontal" flex-row items-center ; label flex-auto, control on the right Checkbox / Switch / Radio rows where label-then-control is the natural reading order.
"responsive" flex-col on small, switches to flex-row at @md/field-group Settings screens where vertical on mobile and horizontal on desktop is the spec. REQUIRES a FieldGroup ancestor : the container-query class @md/field-group:... is keyed to that ancestor's @container/field-group.

ALWAYS wrap Field orientation="responsive" in a FieldGroup. Without the group ancestor the container query has no host and the orientation never switches.

FieldError : Standard Schema integration

FieldError accepts an errors array whose items have the shape { message?: string }. This matches the issue shape emitted by every Standard Schema validator (Zod, Valibot, ArkType). The primitive :

  1. De-duplicates by message.
  2. Renders a single string when one unique message remains.
  3. Renders a <ul> of <li> messages when more than one.
  4. Returns null when errors is empty AND children is empty.
// Single error (typical RHF case)
<FieldError errors={[fieldState.error]} />

// Multiple errors (typical TanStack case)
<FieldError errors={field.state.meta.errors} />

// Custom message (overrides the array)
<FieldError>Something custom went wrong.</FieldError>

ALWAYS pass the error array directly; do NOT pre-stringify. The primitive needs the { message } shape to de-duplicate and to skip empty entries. NEVER pass errors={[fieldState.error.message]} (plain strings) : the de-dup map keys on .message and bare strings produce undefined keys.

Field vs Form composition : decision tree

Are you starting a new form in 2026?
├── YES -> use Field (this skill) + Controller (RHF) OR Field + form.Field (TanStack)
└── NO (existing code already imports from @/components/ui/form)
    ├── Field + FormField in the SAME tree? -> NEVER. Pick one path per form.
    ├── Existing form works? -> keep it on Form composition (shadcn-syntax-form)
    └── Adding a new form alongside? -> Field is fine ; the two coexist at file level
Aspect Field (new) Form composition (legacy)
Form library coupling None ; works with RHF, TanStack Form, or no library Hard-coupled to react-hook-form via FormProvider + Controller
a11y wiring Manual htmlFor + id + aria-invalid + aria-describedby Automatic via useId() + Slot.Root in FormControl
Boilerplate per field ~7 lines (Controller render-prop) ~6 lines (FormField render-prop)
Flexibility Full control over markup, can wrap non-input visuals Constrained to the seven-primitive tree
TanStack Form support First-class Not supported : the layer assumes RHF context
When to pick New code, TanStack Form, mixed component libraries, custom markup Legacy code, quick scaffolds, RHF-only projects

ALWAYS pick ONE path per form. NEVER mix Field and FormField inside the same <form> tree : the two systems do not share id contexts and aria wiring breaks.

Coexistence

Both field.tsx and form.tsx remain in the registry. They live in separate files (@/components/ui/field and @/components/ui/form) and can be installed independently :

npx shadcn@latest add field        # installs Field family only
npx shadcn@latest add form         # installs Form composition only
npx shadcn@latest add field form   # installs both ; pick path per form

ALWAYS install whichever path the existing codebase already uses to avoid mixed conventions. NEVER delete form.tsx from a codebase mid-migration : doing so breaks every legacy form simultaneously.

RSC and 'use client'

The Field primitives use useMemo (in FieldError) and require client rendering when wired to a form library (Controller and form.Field both rely on React context). ALWAYS mark every file that composes Field with a form library as "use client". The field.tsx file itself starts with "use client" at the top of the source.

NEVER place a Controller (or form.Field) inside an RSC : the context provider is in a client component and cannot cross the boundary.

Companion Skills

Reference files

  • references/methods.md : complete API signatures for all ten Field primitives.
  • references/examples.md : Field + Controller (RHF), Field + TanStack Form, Field standalone, FieldSet + FieldLegend + FieldGroup composition, Field with Select / Checkbox controls.
  • references/anti-patterns.md : five canonical anti-patterns with WHY each fails and the fix.

Sources

All claims trace to URLs in SOURCES.md :

Verified 2026-05-19.