Field
Layout primitives for composing accessible form fields — label, description, error, and grouping — without coupling to any form library.
Usage
import {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldSet,
FieldLegend,
FieldContent,
FieldTitle,
FieldSeparator,
} from '@acronis-platform/shadcn-uikit';Basic
A field with a label and description. Wire htmlFor on FieldLabel to the control's id.
This is your public display name.
View sourceHide source
import * as React from 'react';
import {
Field,
FieldLabel,
FieldDescription,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
export function FieldBasic() {
return (
<div className="w-full max-w-sm">
<Field>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input id="username" placeholder="johndoe" />
<FieldDescription>This is your public display name.</FieldDescription>
</Field>
</div>
);
}
With error
FieldError accepts an errors array of { message } objects. It renders nothing when the array is empty or undefined. Set data-invalid="true" on Field to apply the destructive colour to the label.
Must be strong and unique.
- Must be at least 8 characters.
- Must contain at least one number.
View sourceHide source
import * as React from 'react';
import {
Field,
FieldLabel,
FieldDescription,
FieldError,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
export function FieldWithError() {
return (
<div className="w-full max-w-sm space-y-6">
{/* Single error */}
<Field data-invalid="true">
<FieldLabel htmlFor="email-error">Email</FieldLabel>
<Input
id="email-error"
type="email"
defaultValue="notanemail"
aria-invalid
/>
<FieldError
errors={[{ message: 'Please enter a valid email address.' }]}
/>
</Field>
{/* Multiple errors */}
<Field data-invalid="true">
<FieldLabel htmlFor="password-error">Password</FieldLabel>
<Input
id="password-error"
type="password"
defaultValue="abc"
aria-invalid
/>
<FieldDescription>Must be strong and unique.</FieldDescription>
<FieldError
errors={[
{ message: 'Must be at least 8 characters.' },
{ message: 'Must contain at least one number.' },
]}
/>
</Field>
</div>
);
}
Horizontal orientation
The orientation prop controls how the label and control are arranged. "horizontal" places the label on the left and the control on the right. Use FieldContent inside FieldLabel to add a description block beside a toggle control.
View sourceHide source
import * as React from 'react';
import {
Field,
FieldLabel,
FieldContent,
FieldDescription,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Switch } from '@acronis-platform/shadcn-uikit/react';
import { Checkbox } from '@acronis-platform/shadcn-uikit/react';
export function FieldHorizontal() {
return (
<div className="w-full max-w-lg space-y-4">
{/* Label left, control right */}
<Field orientation="horizontal">
<FieldLabel htmlFor="h-name">Display name</FieldLabel>
<Input id="h-name" placeholder="Your name" className="max-w-xs" />
</Field>
{/* With description block */}
<Field orientation="horizontal">
<FieldLabel htmlFor="h-switch">
<FieldContent>
<span>Email notifications</span>
<FieldDescription>
Get notified about account activity.
</FieldDescription>
</FieldContent>
</FieldLabel>
<Switch id="h-switch" defaultChecked />
</Field>
{/* Checkbox horizontal */}
<Field orientation="horizontal">
<FieldLabel htmlFor="h-checkbox">
<FieldContent>
<span>Marketing emails</span>
<FieldDescription>
Receive updates on new features.
</FieldDescription>
</FieldContent>
</FieldLabel>
<Checkbox id="h-checkbox" />
</Field>
</div>
);
}
Available orientations: "vertical" (default) · "horizontal" · "responsive" (vertical on small screens, horizontal at @md).
FieldGroup
FieldGroup stacks multiple Field components with consistent spacing. Use it as the outer wrapper for any multi-field form section.
We will never share your email.
Up to 160 characters.
View sourceHide source
import * as React from 'react';
import {
Field,
FieldLabel,
FieldDescription,
FieldGroup,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Textarea } from '@acronis-platform/shadcn-uikit/react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
export function FieldGroupDemo() {
return (
<div className="w-full max-w-md">
<FieldGroup>
<Field>
<FieldLabel htmlFor="fg-name">Full name</FieldLabel>
<Input id="fg-name" placeholder="Jane Doe" />
</Field>
<Field>
<FieldLabel htmlFor="fg-email">Email</FieldLabel>
<Input id="fg-email" type="email" placeholder="jane@example.com" />
<FieldDescription>We will never share your email.</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="fg-role">Role</FieldLabel>
<Select>
<SelectTrigger id="fg-role">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="fg-bio">Bio</FieldLabel>
<Textarea
id="fg-bio"
placeholder="A short bio…"
className="resize-none"
/>
<FieldDescription>Up to 160 characters.</FieldDescription>
</Field>
<Button>Save</Button>
</FieldGroup>
</div>
);
}
FieldSet
FieldSet renders a native <fieldset> with a FieldLegend. Use it to semantically group related controls (radio buttons, checkbox lists, toggle switches) so screen readers announce the group name before each control.
View sourceHide source
import * as React from 'react';
import {
Field,
FieldLabel,
FieldDescription,
FieldSet,
FieldLegend,
FieldGroup,
FieldContent,
} from '@acronis-platform/shadcn-uikit/react';
import { Switch } from '@acronis-platform/shadcn-uikit/react';
import {
RadioGroup,
RadioGroupItem,
} from '@acronis-platform/shadcn-uikit/react';
export function FieldSetDemo() {
return (
<div className="w-full max-w-md space-y-8">
{/* Switch group */}
<FieldSet>
<FieldLegend>Notifications</FieldLegend>
<FieldGroup>
<Field orientation="horizontal">
<FieldLabel htmlFor="notif-email">
<FieldContent>
<span>Email</span>
<FieldDescription>
Account activity and updates.
</FieldDescription>
</FieldContent>
</FieldLabel>
<Switch id="notif-email" defaultChecked />
</Field>
<Field orientation="horizontal">
<FieldLabel htmlFor="notif-push">
<FieldContent>
<span>Push notifications</span>
<FieldDescription>Critical alerts only.</FieldDescription>
</FieldContent>
</FieldLabel>
<Switch id="notif-push" />
</Field>
<Field orientation="horizontal">
<FieldLabel htmlFor="notif-marketing">
<FieldContent>
<span>Marketing</span>
<FieldDescription>
New features and promotions.
</FieldDescription>
</FieldContent>
</FieldLabel>
<Switch id="notif-marketing" />
</Field>
</FieldGroup>
</FieldSet>
{/* Radio group */}
<FieldSet>
<FieldLegend>Theme</FieldLegend>
<RadioGroup defaultValue="system" className="space-y-2">
<Field orientation="horizontal">
<FieldLabel htmlFor="theme-light">Light</FieldLabel>
<RadioGroupItem id="theme-light" value="light" />
</Field>
<Field orientation="horizontal">
<FieldLabel htmlFor="theme-dark">Dark</FieldLabel>
<RadioGroupItem id="theme-dark" value="dark" />
</Field>
<Field orientation="horizontal">
<FieldLabel htmlFor="theme-system">System</FieldLabel>
<RadioGroupItem id="theme-system" value="system" />
</Field>
</RadioGroup>
</FieldSet>
</div>
);
}
With TanStack Form
Because Field primitives have no dependency on React Hook Form, they compose naturally with TanStack Form. Pass field.state.meta.errors directly to FieldError.
View sourceHide source
import * as React from 'react';
import { useForm } from '@tanstack/react-form';
import * as z from 'zod';
import {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Textarea } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@acronis-platform/shadcn-uikit/react';
const schema = z.object({
username: z
.string()
.min(2, 'At least 2 characters.')
.max(30, 'Max 30 characters.'),
email: z.string().email('Enter a valid email address.'),
role: z.string().min(1, 'Please select a role.'),
bio: z.string().max(160, 'Max 160 characters.').optional(),
});
type FormValues = z.infer<typeof schema>;
export function FieldTanstackForm() {
const [submitted, setSubmitted] = React.useState<FormValues | null>(null);
const form = useForm({
defaultValues: { username: '', email: '', role: '', bio: '' },
onSubmit: async ({ value }) => setSubmitted(value as FormValues),
});
if (submitted) {
return (
<div className="w-full max-w-md rounded-lg border p-6 space-y-3">
<p className="text-sm font-medium text-green-600">Saved!</p>
<pre className="text-xs bg-muted rounded p-3 overflow-auto">
{JSON.stringify(submitted, null, 2)}
</pre>
<Button variant="outline" size="sm" onClick={() => setSubmitted(null)}>
Edit again
</Button>
</div>
);
}
return (
<div className="w-full max-w-md">
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<FieldGroup>
<form.Field
name="username"
validators={{
onChange: ({ value }) => {
const result = schema.shape.username.safeParse(value);
return result.success
? undefined
: result.error.issues[0]?.message;
},
}}
>
{(field) => (
<Field
data-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
? 'true'
: undefined
}
>
<FieldLabel htmlFor={field.name}>Username</FieldLabel>
<Input
id={field.name}
placeholder="johndoe"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
aria-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
}
/>
<FieldDescription>Your public display name.</FieldDescription>
{field.state.meta.isTouched && (
<FieldError
errors={field.state.meta.errors.map((e) => ({
message: e?.toString(),
}))}
/>
)}
</Field>
)}
</form.Field>
<form.Field
name="email"
validators={{
onChange: ({ value }) => {
const result = schema.shape.email.safeParse(value);
return result.success
? undefined
: result.error.issues[0]?.message;
},
}}
>
{(field) => (
<Field
data-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
? 'true'
: undefined
}
>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
id={field.name}
type="email"
placeholder="jane@example.com"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
aria-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
}
/>
{field.state.meta.isTouched && (
<FieldError
errors={field.state.meta.errors.map((e) => ({
message: e?.toString(),
}))}
/>
)}
</Field>
)}
</form.Field>
<form.Field
name="role"
validators={{
onChange: ({ value }) => {
const result = schema.shape.role.safeParse(value);
return result.success
? undefined
: result.error.issues[0]?.message;
},
}}
>
{(field) => (
<Field
data-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
? 'true'
: undefined
}
>
<FieldLabel htmlFor={field.name}>Role</FieldLabel>
<Select
value={field.state.value}
onValueChange={(val) => field.handleChange(val ?? '')}
>
<SelectTrigger id={field.name} onBlur={field.handleBlur}>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
{field.state.meta.isTouched && (
<FieldError
errors={field.state.meta.errors.map((e) => ({
message: e?.toString(),
}))}
/>
)}
</Field>
)}
</form.Field>
<form.Field
name="bio"
validators={{
onChange: ({ value }) => {
const result = schema.shape.bio.safeParse(value);
return result.success
? undefined
: result.error.issues[0]?.message;
},
}}
>
{(field) => (
<Field>
<FieldLabel htmlFor={field.name}>Bio</FieldLabel>
<Textarea
id={field.name}
placeholder="Tell us a little about yourself"
className="resize-none"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
<FieldDescription>Up to 160 characters.</FieldDescription>
{field.state.meta.isTouched && (
<FieldError
errors={field.state.meta.errors.map((e) => ({
message: e?.toString(),
}))}
/>
)}
</Field>
)}
</form.Field>
<form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting]}>
{([canSubmit, isSubmitting]) => (
<Button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? 'Saving…' : 'Save profile'}
</Button>
)}
</form.Subscribe>
</FieldGroup>
</form>
</div>
);
}
Field vs Form
The library ships two parallel sets of form-field primitives:
Field / FieldLabel / FieldError | FormItem / FormLabel / FormMessage | |
|---|---|---|
| Coupling | None — pure layout | React Hook Form (useFormContext) |
| Use with | TanStack Form, native HTML, any lib | React Hook Form only |
| Error source | errors prop (array) | Reads from RHF useFormContext automatically |
| Label ↔ control | Manual htmlFor / id | Auto-wired via FormItem context |
| Orientation variants | vertical · horizontal · responsive | Vertical only |
| Grouping | FieldGroup, FieldSet, FieldLegend | <div className="space-y-4"> |
Rule of thumb: use Field when you are not using React Hook Form. Use FormItem and friends when you are.
API Reference
Field
Prop
Type
FieldError
Accepts the standard div props plus:
| Prop | Type | Description |
|---|---|---|
errors | Array<{ message?: string } | undefined> | Error objects to display. Renders nothing when empty or undefined. |
children | ReactNode | Custom error content. Takes precedence over errors. |
FieldLegend
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "legend" | "label" | "legend" | "legend" uses text-base; "label" uses text-sm for tighter sections. |