Components
Combine labels, controls, and help text to compose accessible form fields and grouped inputs.
Playground
We never share your email with anyone.
Choose a unique username for your account.
Must be at least 8 characters long.
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@workspace/ui/components/field"
import { Input } from "@workspace/ui/components/input"
<FieldGroup>
<Field>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input id="username" placeholder="westy" />
<FieldDescription>
Choose a unique username for your account.
</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<FieldDescription>Must be at least 8 characters long.</FieldDescription>
<Input id="password" type="password" placeholder="••••••••" />
</Field>
</FieldGroup>orientation="horizontal" places the label beside the control — the natural fit for Switch, Checkbox and Radio. When the field also has a description, wrap label and description in a FieldContent so they stay aligned as one column.Folders are synced and accessible from other devices.
<Field orientation="horizontal">
<FieldLabel htmlFor="2fa">Multi-factor authentication</FieldLabel>
<Switch id="2fa" />
</Field>
<Field orientation="horizontal">
<Checkbox id="sync" defaultChecked />
<FieldContent>
<FieldLabel htmlFor="sync">Sync Desktop & Documents folders</FieldLabel>
<FieldDescription>
Folders are synced and accessible from other devices.
</FieldDescription>
</FieldContent>
</Field>orientation="responsive" stacks vertically in narrow containers and switches to a horizontal row when the surrounding FieldGroup is at least 28rem wide. FieldGroup is a container-query root (@container/field-group), so the breakpoint follows the container — not the viewport. Resize the window to see it switch.Provide your full name for identification.
<FieldGroup>
<Field orientation="responsive">
<FieldContent>
<FieldLabel htmlFor="name">Name</FieldLabel>
<FieldDescription>
Provide your full name for identification.
</FieldDescription>
</FieldContent>
<Input id="name" placeholder="Evil Rabbit" required />
</Field>
<Field orientation="responsive">
<Button type="submit">Submit</Button>
<Button type="button" variant="outline">
Cancel
</Button>
</Field>
</FieldGroup><fieldset> named by a FieldLegend, grouping related fields for keyboard and assistive tech users. Compose multi-column rows with a plain grid — each cell is its own Field.<FieldSet>
<FieldLegend>Address Information</FieldLegend>
<FieldDescription>
We need your address to deliver your order.
</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="street">Street Address</FieldLabel>
<Input id="street" placeholder="123 Main St" />
</Field>
<div className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="city">City</FieldLabel>
<Input id="city" placeholder="New York" />
</Field>
<Field>
<FieldLabel htmlFor="zip">Postal Code</FieldLabel>
<Input id="zip" placeholder="90502" />
</Field>
</div>
</FieldGroup>
</FieldSet>variant="label" legend drops to label size so nested groups don't shout over the fields. The same pattern works with Checkbox for multi-select lists.<FieldSet>
<FieldLegend variant="label">Subscription Plan</FieldLegend>
<FieldDescription>
Yearly and lifetime plans offer significant savings.
</FieldDescription>
<RadioGroup defaultValue="monthly">
<Field orientation="horizontal">
<RadioGroupItem value="monthly" id="plan-monthly" />
<FieldLabel htmlFor="plan-monthly" className="font-normal">
Monthly ($9.99/month)
</FieldLabel>
</Field>
<Field orientation="horizontal">
<RadioGroupItem value="yearly" id="plan-yearly" />
<FieldLabel htmlFor="plan-yearly" className="font-normal">
Yearly ($99.99/year)
</FieldLabel>
</Field>
<Field orientation="horizontal">
<RadioGroupItem value="lifetime" id="plan-lifetime" />
<FieldLabel htmlFor="plan-lifetime" className="font-normal">
Lifetime ($299.99)
</FieldLabel>
</Field>
</RadioGroup>
</FieldSet><RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="kubernetes">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Kubernetes</FieldTitle>
<FieldDescription>
Run GPU workloads on a K8s cluster.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="kubernetes" id="kubernetes" />
</Field>
</FieldLabel>
<FieldLabel htmlFor="vm">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>Virtual Machine</FieldTitle>
<FieldDescription>
Access a cluster to run GPU workloads.
</FieldDescription>
</FieldContent>
<RadioGroupItem value="vm" id="vm" />
</Field>
</FieldLabel>
</RadioGroup>aria-label — pair with FieldTitle instead of FieldLabel, since there is no input element to target with htmlFor.Set your budget range ($200 – $800).
<Field>
<FieldTitle>Price Range</FieldTitle>
<FieldDescription>
Set your budget range ($200 – $800).
</FieldDescription>
<Slider
defaultValue={[200, 800]}
max={1000}
step={10}
className="mt-2"
aria-label="Price Range"
/>
</Field><FieldGroup>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" type="email" placeholder="hello@westy.com" />
</Field>
<Field>
<Button type="submit">Sign in</Button>
</Field>
<FieldSeparator>Or continue with</FieldSeparator>
<Field>
<Button variant="outline" type="button">
Continue with Google
</Button>
</Field>
</FieldGroup>data-invalid on the Field to switch the whole block into the destructive state, and aria-invalid on the control for assistive technologies. FieldError renders with role="alert" — it also accepts an errors array (from react-hook-form or any Standard Schema validator) and renders a list automatically when there is more than one message.<Field data-invalid>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" type="email" aria-invalid defaultValue="hello@" />
<FieldError>Enter a valid email address.</FieldError>
</Field>
// With react-hook-form
<FieldError errors={errors.username ? [errors.username] : []} />text-start, ms-*), so a dir="rtl" ancestor is all it needs — labels, descriptions and error lists mirror without extra classes. Radix-based controls inside the field still need their own dir via the DirectionProvider.أدخل رقم البطاقة المكون من 16 رقمًا
<div dir="rtl">
<FieldGroup>
<Field>
<FieldLabel htmlFor="card">رقم البطاقة</FieldLabel>
<Input id="card" placeholder="1234 5678 9012 3456" />
<FieldDescription>أدخل رقم البطاقة المكون من 16 رقمًا</FieldDescription>
</Field>
<Field orientation="horizontal">
<Checkbox id="shipping" defaultChecked />
<FieldLabel htmlFor="shipping" className="font-normal">
نفس عنوان الشحن
</FieldLabel>
</Field>
</FieldGroup>
</div>role="group" so nested controls inherit labeling; FieldSet and FieldLegend keep related controls grouped semantically. Always pair FieldLabel's htmlFor with the control's id, and mirror data-invalid on the Field with aria-invalid on the control. FieldError announces itself via role="alert". Use FieldSeparator sparingly so screen readers encounter clear section boundaries.<Field data-invalid> <FieldLabel htmlFor="email">Email</FieldLabel> <Input id="email" type="email" aria-invalid /> <FieldError>Enter a valid email address.</FieldError> </Field>
| Component | Prop | Default |
|---|---|---|
| Field | orientation?: "vertical" | "horizontal" | "responsive" | "vertical" |
| Field | data-invalid?: boolean | — |
| FieldLegend | variant?: "legend" | "label" | "legend" |
| FieldLabel | asChild?: boolean | false |
| FieldError | errors?: Array<{ message?: string } | undefined> | — |
| FieldSeparator | children?: ReactNode | — |
<FieldSet data-slot="field-set"> <FieldLegend data-slot="field-legend" data-variant="legend|label" /> <FieldGroup data-slot="field-group" /> <Field data-slot="field" role="group" data-orientation="vertical|horizontal|responsive" data-invalid /> <FieldContent data-slot="field-content" /> <FieldLabel data-slot="field-label" /> <FieldTitle data-slot="field-label" /> <FieldDescription data-slot="field-description" /> <FieldSeparator data-slot="field-separator" data-content /> <FieldError data-slot="field-error" role="alert" />
Tokens used
9 design tokens consumed by the Field component.
--destructive
Destructive
--primary
Primary
--muted-foreground
Muted Foreground
--background
Background
--border
Border
--text-sm
Text Small (14px)
--text-base
Text Base (16px)
--radius-md
Radius 0.8× (8px)
--spacing
Spacing unit (4px)