Field helps structure form controls with consistent spacing, semantics, and
validation messaging.
When to use
- Group labels, controls, descriptions, and validation states into a single unit.
- Build complex forms with consistent spacing and responsive alignment.
- Display inline or grouped error messages with predictable styling.
Behavior notes
- Use
Fieldfor each control row andFieldContentfor control + helper text. - Add
data-invalid='true'onFieldto apply invalid-state visuals. FieldErrorcan render passed children or derive messages from anerrorsarray.- Use
FieldSetandFieldLegendto group related controls semantically.
Example
<Field>
<FieldLabel htmlFor='email'>Email</FieldLabel>
<FieldContent>
<Input id='email' placeholder='name@company.com' />
<FieldDescription>
We use this for login and notifications.
</FieldDescription>
<FieldError errors={[{ message: 'Email is required' }]} />
</FieldContent>
</Field>
Preview
Usage
import { Checkbox, Input, RadioGroup, RadioGroupItem } from '@branditdev/ui';
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldSet,
FieldTitle,
} from './field';
export function FieldDemo() {
return (
<FieldSet className='w-full max-w-md'>
<FieldGroup>
<Field>
<FieldLabel htmlFor='name'>Team name</FieldLabel>
<FieldContent>
<Input id='name' defaultValue='Growth Platform' />
<FieldDescription>
This label appears in dashboards and reports.
</FieldDescription>
</FieldContent>
</Field>
<Field>
<FieldLabel htmlFor='notifications'>Notifications</FieldLabel>
<FieldContent>
<div className='flex items-center gap-2'>
<Checkbox id='notifications' defaultChecked />
<span className='text-sm'>Enabled</span>
</div>
<FieldDescription>Email weekly product updates.</FieldDescription>
</FieldContent>
</Field>
<Field>
<FieldContent>
<FieldTitle>Billing cycle</FieldTitle>
<RadioGroup defaultValue='monthly'>
<div className='flex items-center gap-2'>
<RadioGroupItem id='cycle-monthly' value='monthly' />
<FieldLabel htmlFor='cycle-monthly'>Monthly</FieldLabel>
</div>
<div className='flex items-center gap-2'>
<RadioGroupItem id='cycle-annual' value='annual' />
<FieldLabel htmlFor='cycle-annual'>Annual</FieldLabel>
</div>
</RadioGroup>
</FieldContent>
</Field>
<Field data-invalid='true'>
<FieldLabel htmlFor='api-key'>API key</FieldLabel>
<FieldContent>
<Input id='api-key' defaultValue='pk_live_1234' />
<FieldError errors={[{ message: 'API key must start with sk_' }]} />
</FieldContent>
</Field>
</FieldGroup>
</FieldSet>
);
}
API
FieldSet(
props):Element
Defined in: components/field/field.tsx:15
Field UI component.
Parameters
| Name | Type | Optional | Default | Description |
|---|---|---|---|---|
| props | DetailedHTMLProps<FieldsetHTMLAttributes<HTMLFieldSetElement>> | no | - | Component properties. |
Returns
Element
Rendered component.
Source
import { cva, type VariantProps } from 'class-variance-authority';
import { useMemo } from 'react';
import { cn, Label, Separator } from '@branditdev/ui';
export function FieldSet({
className,
...props
}: React.ComponentProps<'fieldset'>) {
return (
<fieldset
data-slot='field-set'
className={cn(
'flex flex-col gap-6',
'has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3',
className
)}
{...props}
/>
);
}
export function FieldLegend({
className,
variant = 'legend',
...props
}: React.ComponentProps<'legend'> & { variant?: 'legend' | 'label' }) {
return (
<legend
data-slot='field-legend'
data-variant={variant}
className={cn(
'mb-3 font-medium',
'data-[variant=legend]:text-base',
'data-[variant=label]:text-sm',
className
)}
{...props}
/>
);
}
export function FieldGroup({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='field-group'
className={cn(
'group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4',
className
)}
{...props}
/>
);
}
const fieldVariants = cva(
'group/field flex w-full gap-3 data-[invalid=true]:text-destructive',
{
variants: {
orientation: {
vertical: ['flex-col [&>*]:w-full [&>.sr-only]:w-auto'],
horizontal: [
'flex-row items-center',
'[&>[data-slot=field-label]]:flex-auto',
'has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
],
responsive: [
'flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto',
'@md/field-group:[&>[data-slot=field-label]]:flex-auto',
'@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px',
],
},
},
defaultVariants: {
orientation: 'vertical',
},
}
);
export function Field({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<'div'> & VariantProps<typeof fieldVariants>) {
return (
<div
role='group'
data-slot='field'
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
);
}
export function FieldContent({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='field-content'
className={cn(
'group/field-content flex flex-1 flex-col gap-1.5 leading-snug',
className
)}
{...props}
/>
);
}
export function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot='field-label'
className={cn(
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50',
'has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border *:data-[slot=field]:p-4',
'has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10',
className
)}
{...props}
/>
);
}
export function FieldTitle({
className,
...props
}: React.ComponentProps<'div'>) {
return (
<div
data-slot='field-label'
className={cn(
'flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50',
className
)}
{...props}
/>
);
}
export function FieldDescription({
className,
...props
}: React.ComponentProps<'p'>) {
return (
<p
data-slot='field-description'
className={cn(
'text-sm leading-normal font-normal text-muted-foreground group-has-data-[orientation=horizontal]/field:text-balance',
'last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5',
'[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
className
)}
{...props}
/>
);
}
export function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<'div'> & {
children?: React.ReactNode;
}) {
return (
<div
data-slot='field-separator'
data-content={!!children}
className={cn(
'relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2',
className
)}
{...props}
>
<Separator className='absolute inset-0 top-1/2' />
{children && (
<span
className='relative mx-auto block w-fit bg-background px-2 text-muted-foreground'
data-slot='field-separator-content'
>
{children}
</span>
)}
</div>
);
}
export function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<'div'> & {
errors?: Array<{ message?: string } | undefined>;
}) {
const content = useMemo(() => {
if (children) {
return children;
}
if (!errors?.length) {
return null;
}
const uniqueErrors = [
...new Map(errors.map(error => [error?.message, error])).values(),
];
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message;
}
return (
<ul className='ml-4 flex list-disc flex-col gap-1'>
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
);
}, [children, errors]);
if (!content) {
return null;
}
return (
<div
role='alert'
data-slot='field-error'
className={cn('text-sm font-normal text-destructive', className)}
{...props}
>
{content}
</div>
);
}