Field
A flat form-field wrapper for the common label, control, description, and error pattern. It keeps validation and form state in your app, while standardizing the visual structure and accessibility wiring around shadcn form controls.
It also re-exports the single-field shadcn slots (FieldContent, FieldLabel, FieldTitle, FieldDescription, and FieldError) so the official single Field examples can move to @/components/easy/field without changing structure.
Used for workspace notifications.
Controls billing and member management access.
Send a short digest every Monday.
Unlock your workspace faster.
Installation
With the @easy-shadcn namespace configured:
pnpm dlx shadcn@latest add @easy-shadcn/fieldOr install via the full URL (zero configuration):
pnpm dlx shadcn@latest add https://easy-shadcn.vercel.app/r/field.jsonThe underlying shadcn field primitive is installed automatically alongside field.
Usage
In flat mode the label/control/description/error wiring is automatic — no manual id, htmlFor, aria-invalid, or aria-describedby needed:
import { Input } from "@/components/ui/input"
import { Field } from "@/components/easy/field"
<Field
label="Email"
description="Used for workspace notifications."
error={errors.email?.message}
required
>
<Input disabled={isSubmitting} />
</Field>Use orientation="horizontal" for checkbox or switch-style rows where the control should sit before the label text:
<Field
label="Weekly summary"
description="Send a short digest every Monday."
orientation="horizontal"
>
<Checkbox />
</Field>For the official shadcn composition style, omit the flat props and compose the single-field slots directly:
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/easy/field"
import { Input } from "@/components/ui/input"
<Field invalid>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input id="username" autoComplete="off" aria-invalid />
<FieldDescription>Choose a unique username.</FieldDescription>
<FieldError>Choose another username.</FieldError>
</Field>React Hook Form + Zod
Pass the field error message (or the issues array) straight from the form library — Field derives the invalid state and the screen-reader wiring from it:
API
Field
Field supports two modes:
- Flat mode: pass
label,description, orerror. - Composition mode: omit those props and use the official Field slots as children.
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | - | Field label. Auto-associated with the control in flat mode. |
children | ReactNode | - | The control, such as Input, Textarea, Select, or DatePicker. |
description | ReactNode | - | Helper text below the control, or beside it in horizontal mode. |
error | ReactNode | Array<{ message?: string }> | - | Validation message, or a form-library issues array (deduplicated, rendered as a list when multiple). When present, the wrapper gets data-invalid and the control gets aria-invalid. |
invalid | boolean | derived from error | Forces the owned root invalid marker and flat-control aria-invalid without rendering an error message. |
disabled | boolean | false | Adds disabled state to the field wrapper. Pass disabled to the control too — it is intentionally not injected. |
required | boolean | false | Appends a required indicator to the label. |
htmlFor | string | - | Overrides the auto-generated control id used by the label. |
orientation | "vertical" | "horizontal" | "responsive" | "vertical" | Vertical renders label above the control. Horizontal and responsive render the control before the text for checkbox/switch-style fields. |
className | ClassValue | - | Wrapper className. |
contentClassName | ClassValue | - | Text/content wrapper className. |
labelClassName | ClassValue | - | Label className. |
descriptionClassName | ClassValue | - | Description className. |
errorClassName | ClassValue | - | Error className. |
Accessibility wiring
In flat mode, when children is a single element, Field injects:
id— resolved fromhtmlFor, the child's ownid, or an auto-generated one (in that order), then written back to the control so the label cannot point at a different id.aria-describedby— the generated description and error ids followed by any caller-provided tokens, with duplicates removed.aria-invalid— forced totruewhenerrororinvalidmarks the Field invalid; otherwise the child's value is preserved.
Only those three control props are managed. The child's ref, events, role, data attributes, content, and behavior remain caller-owned. When children is not a single non-Fragment element, nothing is injected — wire the attributes yourself. disabled is never injected because it changes behavior, not just semantics.
The injection only works when the child forwards id / aria-* to its underlying DOM control — native elements and shadcn inputs do.
The root wrapper owns its group role, slot/orientation markers, and disabled/invalid state markers. Use disabled, invalid, and orientation instead of setting their derived ARIA/data attributes. Ordinary root props still pass through, including refs, events, id, accessible naming/description, and unrelated data-*. A root id or aria-describedby describes the group itself; to add an external description to the control, put aria-describedby on the child and Field will merge it with its generated ids.
Single-field slots
FieldContent, FieldLabel, FieldTitle, FieldDescription, and FieldError are re-exported from the underlying shadcn primitive.
FieldError accepts an errors array — zod issues and react-hook-form errors are structurally compatible:
<FieldError errors={result.error.issues} />When to use the primitive instead
Use components/ui/field directly when you want the raw shadcn primitive and no easy-shadcn flat-props layer at all — for example multi-control fields or fully custom layouts.
Grouped examples from the official shadcn docs (FieldSet, FieldGroup, FieldLegend, and FieldSeparator) are intentionally left to the primitive for now. This component focuses on the single Field shape.