Field
A form field wrapper that composes Label, input elements, helper text, and error message into a single accessible unit with automatic ID generation.
AccessibleDark ModeAuto-IDError State
Install
$
npx react-principles add field01
Live Demo
Explore all variants and interactive states in Storybook.
Open Storybookopen_in_newWe'll never share your email.
Must be at least 8 characters.
Username is already taken.
expand_more
Tell us about yourself.
This field is disabled.
02
Code Snippet
src/ui/Field.tsx
import { Field } from "@/ui/Field"; import { Input } from "@/ui/Input"; // Basic <Field label="Email"> <Input type="email" placeholder="you@example.com" /> </Field> // With helper text <Field label="Password" helperText="Must be at least 8 characters." required > <Input type="password" /> </Field> // With error message <Field label="Username" errorMessage="Username is already taken." > <Input placeholder="Choose a username" /> </Field>
03
Copy-Paste (Single File)
Field.tsx
import { cloneElement, forwardRef, type ReactNode, type HTMLAttributes } from "react"; import { Label } from "./Label"; import { cn } from "@/lib/utils"; // ─── Types ──────────────────────────────────────────────────────────────────── export interface FieldProps extends HTMLAttributes<HTMLDivElement> { label?: string; helperText?: string; errorMessage?: string; required?: boolean; disabled?: boolean; id?: string; children: ReactNode; } // ─── Component ──────────────────────────────────────────────────────────────── export const Field = forwardRef<HTMLDivElement, FieldProps>(function FieldRoot( { label, helperText, errorMessage, required, disabled, id, className, children, ...rest }, ref ) { // Auto-generate ID from label if not provided const fieldId = id ?? (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined); const descriptionId = fieldId ? `${fieldId}-description` : undefined; // Clone child element and inject accessibility props const child = cloneElement(children as React.ReactElement, { id: fieldId, "aria-describedby": descriptionId, "aria-invalid": !!errorMessage, } as Record<string, unknown>); return ( <div ref={ref} className={cn("flex flex-col gap-1.5", disabled && "opacity-50", className)} {...rest} > {label && ( <Label htmlFor={fieldId} required={required}> {label} </Label> )} {child} {(helperText || errorMessage) && descriptionId && ( <p id={descriptionId} className={cn( "text-xs", errorMessage ? "text-red-500 dark:text-red-400" : "text-slate-500 dark:text-slate-400" )} > {errorMessage || helperText} </p> )} </div> ); });
04
Props
Extends all native HTMLDivElement attributes.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Field label text. |
helperText | string | — | Descriptive helper text shown below input. |
errorMessage | string | — | Error message — replaces helperText when present. |
required | boolean | false | Shows required indicator on label. |
disabled | boolean | false | Applies muted opacity style. |
id | string | auto-generated | ID for label-input association. |