Easy Shadcn
Components

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.

Touch ID

Unlock your workspace faster.

Installation

With the @easy-shadcn namespace configured:

pnpm dlx shadcn@latest add @easy-shadcn/field

Or install via the full URL (zero configuration):

pnpm dlx shadcn@latest add https://easy-shadcn.vercel.app/r/field.json

The 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 data-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:

At least 8 characters.

API

Field

Field supports two modes:

  • Flat mode: pass label, description, or error.
  • Composition mode: omit those props and use the official Field slots as children.
PropTypeDefaultDescription
labelReactNode-Field label. Auto-associated with the control in flat mode.
childrenReactNode-The control, such as Input, Textarea, Select, or DatePicker.
descriptionReactNode-Helper text below the control, or beside it in horizontal mode.
errorReactNode | 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.
invalidbooleanderived from errorForces invalid styling without rendering an error message.
disabledbooleanfalseAdds disabled state to the field wrapper. Pass disabled to the control too — it is intentionally not injected.
requiredbooleanfalseAppends a required indicator to the label.
htmlForstring-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.
classNameClassValue-Wrapper className.
contentClassNameClassValue-Text/content wrapper className.
labelClassNameClassValue-Label className.
descriptionClassNameClassValue-Description className.
errorClassNameClassValue-Error className.

Accessibility wiring

In flat mode, when children is a single element, Field injects:

  • id — from htmlFor, the child's own id, or an auto-generated one (in that order); the label points at it.
  • aria-describedby — referencing the description and error nodes.
  • aria-invalid — when error or invalid is set.

Explicit props on the child always win over the injected ones. When children is not a single element (fragments, multiple controls), 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. If you pass both htmlFor and a child id, keep them identical (or pass just one), otherwise the label points at htmlFor while the control keeps its own id.

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.

On this page