Field
A component that provides labelling and validation for form controls. Built with Base UI and Tailwind CSS. Copy-paste ready.
Visible on your profile
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Field>
<FieldLabel>Name</FieldLabel>
<Input placeholder="Enter your name" type="text" />
<FieldDescription>Visible on your profile</FieldDescription>
</Field>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/field
Usage
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
FieldValidity,
} from "@/components/ui/field"
import { Input } from "@/components/ui/input"<Field>
<FieldLabel>Name</FieldLabel>
<Input type="text" placeholder="Enter your name" />
<FieldDescription>Visible on your profile</FieldDescription>
<FieldError>Please enter a valid name</FieldError>
<FieldValidity>
{(validity) => (
{validity.error && <p>{validity.error}</p>}
)}
</FieldValidity>
</Field>Examples
Required Field
Add the required prop to the control inside Field to display a required indicator on the label and enable browser-native constraint validation.
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Field>
<FieldLabel>
Password <span className="text-destructive-foreground">*</span>
</FieldLabel>
<Input placeholder="Enter password" required type="password" />
<FieldError>Please fill out this field.</FieldError>
</Field>
);
}
With Error
Enter an invalid email address and press enter to see the error.
This field is currently disabled.
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Field disabled>
<FieldLabel>Email</FieldLabel>
<Input disabled placeholder="Enter your email" type="email" />
<FieldDescription>This field is currently disabled.</FieldDescription>
</Field>
);
}
With Validity
Use FieldValidity to render custom UI based on the current validation state — such as a checkmark when the field is valid.
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function FieldWithErrorDemo() {
return (
<Field>
<FieldLabel>Email</FieldLabel>
<Input placeholder="Enter your email" type="email" />
<FieldError>Please enter a valid email address.</FieldError>
</Field>
);
}
Input Group
Wrap an InputGroup inside Field to get label association and error messaging for complex input compositions.
{
"state": {
"badInput": false,
"customError": false,
"patternMismatch": false,
"rangeOverflow": false,
"rangeUnderflow": false,
"stepMismatch": false,
"tooLong": false,
"tooShort": false,
"typeMismatch": false,
"valid": null,
"valueMissing": false
},
"error": "",
"errors": [],
"value": null,
"initialValue": null,
"validity": {
"badInput": false,
"customError": false,
"patternMismatch": false,
"rangeOverflow": false,
"rangeUnderflow": false,
"stepMismatch": false,
"tooLong": false,
"tooShort": false,
"typeMismatch": false,
"valid": null,
"valueMissing": false
}
}"use client";
import { Field, FieldLabel, FieldValidity } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function FieldWithValidityDemo() {
return (
<Field>
<FieldLabel>Email</FieldLabel>
<Input placeholder="Enter your email" required type="email" />
<FieldValidity>
{(validity) => (
<div className="flex w-full flex-col gap-2">
{validity.error && (
<p className="text-destructive-foreground text-xs">
{validity.error}
</p>
)}
<div className="w-full rounded-md bg-muted p-2">
<pre className="scrollbar-none max-h-60 overflow-y-auto font-mono text-xs">
{JSON.stringify(validity, null, 2)}
</pre>
</div>
</div>
)}
</FieldValidity>
</Field>
);
}
Autocomplete Field
Use Field with Autocomplete to wire up an accessible label, required validation, and visible error for suggestion inputs.
import { ArrowRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/components/ui/input-group";
export default function Particle() {
return (
<Field>
<FieldLabel>Subscribe</FieldLabel>
<InputGroup>
<InputGroupInput placeholder="Your best email" type="email" />
<InputGroupAddon align="inline-end">
<Button aria-label="Subscribe" size="icon-xs" variant="ghost">
<ArrowRightIcon aria-hidden="true" />
</Button>
</InputGroupAddon>
</InputGroup>
<FieldError>Please enter a valid email address.</FieldError>
</Field>
);
}
Combobox Field
Use Field with Combobox for single-selection dropdowns that need accessible labelling and form validation.
Select a item.
"use client";
import {
Autocomplete,
AutocompleteEmpty,
AutocompleteInput,
AutocompleteItem,
AutocompleteList,
AutocompletePopup,
} from "@/components/ui/autocomplete";
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Field>
<FieldLabel>Fruits</FieldLabel>
<Autocomplete items={items}>
<AutocompleteInput
aria-label="Search items"
placeholder="Search items…"
/>
<AutocompletePopup>
<AutocompleteEmpty>No items found.</AutocompleteEmpty>
<AutocompleteList>
{(item) => (
<AutocompleteItem key={item.value} value={item}>
{item.label}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompletePopup>
</Autocomplete>
<FieldDescription>Select a item.</FieldDescription>
</Field>
);
}
Combobox Multi-select
Combines Field with a multi-select Combobox so chip-based selections are validated and described accessibly.
Select a item.
"use client";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Field>
<FieldLabel>Fruits</FieldLabel>
<Combobox items={items}>
<ComboboxInput
aria-label="Select an item"
placeholder="Select an item..."
/>
<ComboboxPopup>
<ComboboxEmpty>No results found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
<FieldDescription>Select a item.</FieldDescription>
</Field>
);
}
Textarea Field
Wraps a Textarea in Field to provide a label, description, and character-count error for multi-line inputs.
Select multiple items.
"use client";
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxValue,
} from "@/components/ui/combobox";
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Field>
<FieldLabel>Fruits</FieldLabel>
<Combobox defaultValue={[items[0], items[4]]} items={items} multiple>
<ComboboxChips>
<ComboboxValue>
{(value: { value: string; label: string }[]) => (
<>
{value?.map((item) => (
<ComboboxChip aria-label={item.label} key={item.value}>
{item.label}
</ComboboxChip>
))}
<ComboboxChipsInput
aria-label="Select items"
placeholder={value.length > 0 ? undefined : "Select items…"}
/>
</>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxPopup>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
<FieldDescription>Select multiple items.</FieldDescription>
</Field>
);
}
Select Field
Pairs Field with Select for validated dropdown fields with visible labels and inline error messages.
Write a short bio. Maximum 500 characters.
"use client";
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { Textarea } from "@/components/ui/textarea";
export default function Particle() {
return (
<Field>
<FieldLabel>Bio</FieldLabel>
<Textarea placeholder="Tell us about yourself…" />
<FieldDescription>
Write a short bio. Maximum 500 characters.
</FieldDescription>
</Field>
);
}
Checkbox Field
A single checkbox wrapped in Field to associate an accessible label with the control and show validation feedback.
This is an optional field
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const items = [
{ label: "Select a country", value: null },
{ label: "United States", value: "us" },
{ label: "United Kingdom", value: "uk" },
{ label: "Canada", value: "ca" },
{ label: "Australia", value: "au" },
];
export default function Particle() {
return (
<Field>
<FieldLabel>Country</FieldLabel>
<Select items={items}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectPopup>
{items.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectPopup>
</Select>
<FieldDescription>This is an optional field</FieldDescription>
</Field>
);
}
Checkbox Group Field
A CheckboxGroup wrapped in Field and Fieldset for group-level labelling, description, and validation.
import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldLabel } from "@/components/ui/field";
export default function Particle() {
return (
<Field>
<FieldLabel>
<Checkbox />
Accept terms and conditions
</FieldLabel>
</Field>
);
}
Radio Group Field
A RadioGroup inside Field and Fieldset to validate that the user has selected one of the available options.
"use client";
import { Checkbox } from "@/components/ui/checkbox";
import { CheckboxGroup } from "@/components/ui/checkbox-group";
import { Field, FieldItem, FieldLabel } from "@/components/ui/field";
import { Fieldset, FieldsetLegend } from "@/components/ui/fieldset";
export default function Particle() {
return (
<Field
className="gap-2"
name="frameworks"
render={(props) => <Fieldset {...props} />}
>
<FieldsetLegend className="font-medium text-sm">
Frameworks
</FieldsetLegend>
<CheckboxGroup defaultValue={["react"]}>
<FieldItem>
<FieldLabel>
<Checkbox value="react" /> React
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel>
<Checkbox value="vue" /> Vue
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel>
<Checkbox value="svelte" /> Svelte
</FieldLabel>
</FieldItem>
</CheckboxGroup>
</Field>
);
}
Switch Field
A Switch inside Field for accessible toggle fields that need a visible label and optional error messaging.
"use client";
import {
Field,
FieldDescription,
FieldItem,
FieldLabel,
} from "@/components/ui/field";
import { Fieldset, FieldsetLegend } from "@/components/ui/fieldset";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
export default function Particle() {
return (
<Field
className="gap-2"
name="plan"
render={(props) => <Fieldset {...props} />}
>
<FieldsetLegend className="font-medium text-sm">
Choose Plan
</FieldsetLegend>
<RadioGroup defaultValue="free">
<FieldItem>
<FieldLabel>
<Radio value="free" /> Free
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel>
<Radio value="pro" /> Pro
</FieldLabel>
</FieldItem>
<FieldItem>
<FieldLabel>
<Radio value="enterprise" /> Enterprise
</FieldLabel>
</FieldItem>
</RadioGroup>
<FieldDescription>Select the plan that fits your needs.</FieldDescription>
</Field>
);
}
Slider Field
Wraps a Slider in Field to provide an accessible label and connect the value to form validation.
This is an optional field
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { Slider } from "@/components/ui/slider";
export default function Particle() {
return (
<Field className="items-stretch gap-3">
<FieldLabel>Country</FieldLabel>
<Slider defaultValue={50} />
<FieldDescription>This is an optional field</FieldDescription>
</Field>
);
}
Number Field
A NumberField inside Field with a label and error message for validated numeric inputs.
Choose a value between 1 and 100.
import { Field, FieldDescription } from "@/components/ui/field";
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldScrubArea,
} from "@/components/ui/number-field";
export default function Particle() {
return (
<Field>
<NumberField defaultValue={1} max={100} min={1}>
<NumberFieldScrubArea label="Quantity" />
<NumberFieldGroup>
<NumberFieldDecrement />
<NumberFieldInput />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
<FieldDescription>Choose a value between 1 and 100.</FieldDescription>
</Field>
);
}
Complete Form
A multi-field form showcasing how Field composes with various input types, all wired up with labels, descriptions, and error messages.
"use client";
import type { FormEvent } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function Particle() {
const [loading, setLoading] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
setLoading(true);
await new Promise((r) => setTimeout(r, 800));
setLoading(false);
const data = {
email: formData.get("email"),
fullName: formData.get("fullName"),
newsletter: formData.get("newsletter"),
role: formData.get("role"),
};
alert(
`Full name: ${data.fullName || ""}\nEmail: ${data.email || ""}\nRole: ${
data.role || ""
}\nNewsletter: ${data.newsletter}`,
);
};
return (
<Form className="flex w-full flex-col gap-4" onSubmit={onSubmit}>
<Field name="fullName">
<FieldLabel>
Full Name <span className="text-destructive">*</span>
</FieldLabel>
<Input placeholder="John Doe" required type="text" />
<FieldError>Please enter a valid name.</FieldError>
</Field>
<Field name="email">
<FieldLabel>
Email <span className="text-destructive">*</span>
</FieldLabel>
<Input placeholder="john@example.com" required type="email" />
<FieldError>Please enter a valid email.</FieldError>
</Field>
<Field name="role">
<FieldLabel>Role</FieldLabel>
<Select
items={[
{ label: "Select your role", value: null },
{ label: "Developer", value: "developer" },
{ label: "Designer", value: "designer" },
{ label: "Product Manager", value: "manager" },
{ label: "Other", value: "other" },
]}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectItem value="developer">Developer</SelectItem>
<SelectItem value="designer">Designer</SelectItem>
<SelectItem value="manager">Product Manager</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectPopup>
</Select>
<FieldDescription>This is an optional field</FieldDescription>
</Field>
<Field name="newsletter">
<div className="flex items-center gap-2">
<Checkbox />
<FieldLabel className="cursor-pointer">
Subscribe to newsletter
</FieldLabel>
</div>
</Field>
<Button loading={loading} type="submit">
Submit
</Button>
</Form>
);
}
Switch Field
An inline Switch toggle with a label and description, laid out side-by-side using flex-row for compact preference controls.
Receive emails about new products, features, and updates.
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { Switch } from "@/components/ui/switch";
export default function Particle() {
return (
<Field className="flex-row items-center justify-between rounded-lg border p-4">
<div className="flex flex-col gap-1">
<FieldLabel className="cursor-pointer">Marketing emails</FieldLabel>
<FieldDescription>
Receive emails about new products, features, and updates.
</FieldDescription>
</div>
<Switch />
</Field>
);
}
Date Field
A native date Input wrapped in Field to provide an accessible label, description, and inline error for date-of-birth or deadline inputs.
Used to verify your age and identity.
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Field>
<FieldLabel>Date of birth</FieldLabel>
<Input type="date" />
<FieldDescription>Used to verify your age and identity.</FieldDescription>
<FieldError>Please enter a valid date.</FieldError>
</Field>
);
}
OTP Field
An OtpField inside Field for verification-code entry, with label, description, and an error message when the code is invalid.
Enter the 6-digit code sent to your phone.
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import {
OTPField,
OTPFieldInput,
OTPFieldSeparator,
} from "@/components/ui/otp-field";
export default function Particle() {
return (
<Field>
<FieldLabel>Verification code</FieldLabel>
<OTPField length={6}>
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldSeparator />
<OTPFieldInput />
<OTPFieldInput />
<OTPFieldInput />
</OTPField>
<FieldDescription>
Enter the 6-digit code sent to your phone.
</FieldDescription>
<FieldError>Invalid verification code. Please try again.</FieldError>
</Field>
);
}
File Upload Field
A type="file" input inside Field with an accept attribute, description of allowed formats, and an error message for invalid uploads.
PDF or Word document, max 5 MB.
import { UploadIcon } from "lucide-react";
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Field>
<FieldLabel>
<span className="flex items-center gap-1.5">
<UploadIcon aria-hidden="true" className="size-4 opacity-60" />
Upload resume
</span>
</FieldLabel>
<Input accept=".pdf,.doc,.docx" type="file" />
<FieldDescription>PDF or Word document, max 5 MB.</FieldDescription>
<FieldError>Please upload a valid file.</FieldError>
</Field>
);
}
Password Strength
Uses FieldValidity to render a live strength meter — four coloured bars and a label that update as the user types their new password.
"use client";
import { Field, FieldLabel, FieldValidity } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Field>
<FieldLabel>
New password <span className="text-destructive-foreground">*</span>
</FieldLabel>
<Input
minLength={8}
placeholder="Enter new password"
required
type="password"
/>
<FieldValidity>
{(validity) => {
const value = validity.value as string | undefined;
const len = value?.length ?? 0;
const strength =
len === 0 ? 0 : len < 6 ? 1 : len < 10 ? 2 : len < 14 ? 3 : 4;
const labels = ["", "Weak", "Fair", "Good", "Strong"];
const colors = [
"",
"bg-destructive",
"bg-yellow-500",
"bg-blue-500",
"bg-green-500",
];
return (
<div className="flex flex-col gap-1.5">
<div className="flex gap-1">
{[1, 2, 3, 4].map((level) => (
<div
className={`h-1.5 flex-1 rounded-full transition-colors ${
level <= strength ? colors[strength] : "bg-muted"
}`}
key={level}
/>
))}
</div>
{strength > 0 && (
<p className="text-muted-foreground text-xs">
{labels[strength]} password
</p>
)}
</div>
);
}}
</FieldValidity>
</Field>
);
}
On This Page

