Field
A shadcn-style field component built with Ark UI primitives.
Visible on your profile
import {
Field,
FieldDescription,
FieldInput,
FieldLabel,
} from "@/components/ui/field";
const FieldBasicDemo = () => (
<Field className="max-w-xs">
<FieldLabel>Name</FieldLabel>
<FieldInput placeholder="Enter name" />
<FieldDescription>Visible on your profile</FieldDescription>
</Field>
);
export default FieldBasicDemo;
Installation
npx shadcn@latest add @ark-cn/fieldInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/field.tsx
"use client";
import { Field as FieldPrimitive } from "@ark-ui/react/field";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
export const Field = ({ className, ...props }: FieldPrimitive.RootProps) => (
<FieldPrimitive.Root
className={cn("group/field flex w-full flex-col gap-1.5", className)}
data-slot="field"
{...props}
/>
);
export const FieldLabel = ({
className,
...props
}: FieldPrimitive.LabelProps) => (
<FieldPrimitive.Label
data-slot="field-label"
className={cn(
"text-sm font-medium text-foreground leading-none select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 group-data-disabled/field:pointer-events-none group-data-disabled/field:opacity-64",
className,
)}
{...props}
/>
);
export const FieldDescription = ({
className,
...props
}: FieldPrimitive.HelperTextProps) => (
<FieldPrimitive.HelperText
data-slot="field-description"
className={cn("text-muted-foreground text-xs", className)}
{...props}
/>
);
export const FieldError = ({
className,
...props
}: FieldPrimitive.ErrorTextProps) => (
<FieldPrimitive.ErrorText
data-slot="field-error"
className={cn("text-destructive text-xs", className)}
{...props}
/>
);
export const FieldRequiredIndicator = ({
className,
fallback = "*",
...props
}: FieldPrimitive.RequiredIndicatorProps) => (
<FieldPrimitive.RequiredIndicator
data-slot="field-required-indicator"
fallback={fallback}
className={cn("text-destructive ms-0.5", className)}
{...props}
/>
);
/** Registers a custom control (e.g. styled `Input`) with field context — use `asChild`. */
export const FieldInput = (props: FieldPrimitive.InputProps) => {
if (props.asChild) {
return <FieldPrimitive.Input data-slot="field-control" {...props} />;
}
return (
<FieldPrimitive.Input data-slot="field-input" {...props} asChild>
<Input />
</FieldPrimitive.Input>
);
};
export const FieldItem = (props: FieldPrimitive.ItemProps) => (
<FieldPrimitive.Item {...props} />
);
export const FieldContext = FieldPrimitive.Context;
export type { UseFieldProps, UseFieldReturn } from "@ark-ui/react/field";
export {
FieldRootProvider,
useField,
useFieldContext,
} from "@ark-ui/react/field";
Update import aliases to match your project setup.
Usage
import * as Field from "@/components/ui/field"Read exported parts in src/components/ui/field.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
Visible on your profile
import {
Field,
FieldDescription,
FieldInput,
FieldLabel,
} from "@/components/ui/field";
const FieldBasicDemo = () => (
<Field className="max-w-xs">
<FieldLabel>Name</FieldLabel>
<FieldInput placeholder="Enter name" />
<FieldDescription>Visible on your profile</FieldDescription>
</Field>
);
export default FieldBasicDemo;
Checkbox
Normal
import { Checkbox, CheckboxRoot } from "@/components/ui/checkbox";
import { Field, FieldLabel } from "@/components/ui/field";
const FieldCheckboxDemo = () => (
<Field className="max-w-md">
<FieldLabel className="flex cursor-pointer items-center gap-2 font-normal">
<CheckboxRoot>
<Checkbox />
</CheckboxRoot>
Accept terms and conditions
</FieldLabel>
</Field>
);
export default FieldCheckboxDemo;
Invalid
import { Checkbox, CheckboxRoot } from "@/components/ui/checkbox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
const FieldCheckboxInvalidDemo = () => (
<Field className="max-w-md" invalid>
<FieldLabel className="flex cursor-pointer items-center gap-2 font-normal">
<CheckboxRoot invalid>
<Checkbox />
</CheckboxRoot>
Accept terms and conditions
</FieldLabel>
<FieldError>You must accept the terms and conditions.</FieldError>
</Field>
);
export default FieldCheckboxInvalidDemo;
Invalid
import {
Field,
FieldError,
FieldInput,
FieldLabel,
} from "@/components/ui/field";
const FieldInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<FieldLabel>Email</FieldLabel>
<FieldInput placeholder="Enter your email" type="email" />
<FieldError>Please enter a valid email address.</FieldError>
</Field>
);
export default FieldInvalidDemo;
Select
Normal
React
Solid
Vue
import { createListCollection } from "@ark-ui/react/collection";
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
Select,
SelectControl,
SelectIndicator,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const frameworks = createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
],
});
const FieldSelectNormalDemo = () => (
<Field className="max-w-xs">
<FieldLabel>Framework</FieldLabel>
<Select collection={frameworks} defaultValue={["react"]}>
<SelectControl>
<SelectTrigger asChild>
<Button variant="outline">
<SelectValue placeholder="Pick a framework" />
<SelectIndicator />
</Button>
</SelectTrigger>
</SelectControl>
<SelectPopup>
<SelectList>
{frameworks.items.map((item) => (
<SelectItem item={item} key={item.value}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
<FieldDescription>Choose your preferred UI framework.</FieldDescription>
</Field>
);
export default FieldSelectNormalDemo;
Invalid
Low
Medium
High
import { createListCollection } from "@ark-ui/react/collection";
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import {
Select,
SelectControl,
SelectIndicator,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectList,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const priorities = createListCollection({
items: [
{ label: "Low", value: "low" },
{ label: "Medium", value: "medium" },
{ label: "High", value: "high" },
],
});
const FieldSelectInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<FieldLabel>Priority</FieldLabel>
<Select collection={priorities} invalid>
<SelectControl>
<SelectTrigger asChild>
<Button variant="outline">
<SelectValue placeholder="Select a priority" />
<SelectIndicator />
</Button>
</SelectTrigger>
</SelectControl>
<SelectPopup>
<SelectList>
{priorities.items.map((item) => (
<SelectItem item={item} key={item.value}>
<SelectItemText>{item.label}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectList>
</SelectPopup>
</Select>
<FieldError>Please select a priority before submitting.</FieldError>
</Field>
);
export default FieldSelectInvalidDemo;
Combobox
Normal
San Francisco
New York
Tokyo
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxItemText,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
const cities = [
{ label: "San Francisco", value: "sf" },
{ label: "New York", value: "nyc" },
{ label: "Tokyo", value: "tokyo" },
] as const;
const FieldComboboxNormalDemo = () => (
<Field className="max-w-xs">
<FieldLabel>City</FieldLabel>
<Combobox items={cities}>
<ComboboxInput placeholder="Search a city..." />
<ComboboxPopup>
<ComboboxEmpty>No city found.</ComboboxEmpty>
<ComboboxList items={cities}>
{(item) => (
<ComboboxItem item={item}>
<ComboboxItemText>{item.label}</ComboboxItemText>
<ComboboxItemIndicator />
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
<FieldDescription>Type to filter available options.</FieldDescription>
</Field>
);
export default FieldComboboxNormalDemo;
Invalid
Design
Engineering
Marketing
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxItemIndicator,
ComboboxItemText,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
const teams = [
{ label: "Design", value: "design" },
{ label: "Engineering", value: "engineering" },
{ label: "Marketing", value: "marketing" },
] as const;
const FieldComboboxInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<FieldLabel>Team</FieldLabel>
<Combobox invalid items={teams}>
<ComboboxInput placeholder="Choose a team..." />
<ComboboxPopup>
<ComboboxEmpty>No team found.</ComboboxEmpty>
<ComboboxList items={teams}>
{(item) => (
<ComboboxItem item={item}>
<ComboboxItemText>{item.label}</ComboboxItemText>
<ComboboxItemIndicator />
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
<FieldError>Please select one team.</FieldError>
</Field>
);
export default FieldComboboxInvalidDemo;
Editable
Normal
Ark CN
import {
Editable,
EditableArea,
EditableControl,
EditableEditTrigger,
EditableInput,
EditablePreview,
} from "@/components/ui/editable";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
const FieldEditableNormalDemo = () => (
<Field className="max-w-xs">
<FieldLabel>Display name</FieldLabel>
<Editable defaultValue="Ark CN" placeholder="Enter your display name">
<EditableArea>
<EditableInput />
<EditablePreview />
</EditableArea>
<EditableControl>
<EditableEditTrigger>Edit</EditableEditTrigger>
</EditableControl>
</Editable>
<FieldDescription>
Click edit to update your public profile name.
</FieldDescription>
</Field>
);
export default FieldEditableNormalDemo;
Invalid
lowercase-hyphen-only
import {
Editable,
EditableArea,
EditableControl,
EditableEditTrigger,
EditableInput,
EditablePreview,
} from "@/components/ui/editable";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
const FieldEditableInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<FieldLabel>Project slug</FieldLabel>
<Editable invalid placeholder="lowercase-hyphen-only">
<EditableArea>
<EditableInput />
<EditablePreview />
</EditableArea>
<EditableControl>
<EditableEditTrigger>Edit</EditableEditTrigger>
</EditableControl>
</Editable>
<FieldError>Use lowercase letters, numbers, and hyphens only.</FieldError>
</Field>
);
export default FieldEditableInvalidDemo;
Number Input
Normal
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
NumberFieldInput,
NumberFieldRoot,
} from "@/components/ui/number-field";
const FieldNumberInputNormalDemo = () => (
<Field className="max-w-xs">
<FieldLabel>Age</FieldLabel>
<NumberFieldRoot min={18} max={100} defaultValue="24">
<NumberFieldInput />
</NumberFieldRoot>
<FieldDescription>Age must be between 18 and 100.</FieldDescription>
</Field>
);
export default FieldNumberInputNormalDemo;
Invalid
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import {
NumberFieldInput,
NumberFieldRoot,
} from "@/components/ui/number-field";
const FieldNumberInputInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<FieldLabel>Seats</FieldLabel>
<NumberFieldRoot defaultValue="24" invalid min={1}>
<NumberFieldInput />
</NumberFieldRoot>
<FieldError>Please enter at least 1 seat.</FieldError>
</Field>
);
export default FieldNumberInputInvalidDemo;
Password Input
Normal
import {
Field,
FieldDescription,
FieldInput,
FieldLabel,
} from "@/components/ui/field";
import {
PasswordInput,
PasswordInputRoot,
} from "@/components/ui/password-input";
const FieldPasswordInputNormalDemo = () => (
<Field className="max-w-xs">
<FieldLabel>Password</FieldLabel>
<PasswordInputRoot>
<FieldInput asChild>
<PasswordInput placeholder="Enter your password" />
</FieldInput>
</PasswordInputRoot>
<FieldDescription>
Use at least 8 characters for stronger security.
</FieldDescription>
</Field>
);
export default FieldPasswordInputNormalDemo;
Invalid
import {
Field,
FieldError,
FieldInput,
FieldLabel,
} from "@/components/ui/field";
import {
PasswordInput,
PasswordInputRoot,
} from "@/components/ui/password-input";
const FieldPasswordInputInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<FieldLabel>Password</FieldLabel>
<PasswordInputRoot invalid>
<FieldInput asChild>
<PasswordInput defaultValue="1234" placeholder="Enter your password" />
</FieldInput>
</PasswordInputRoot>
<FieldError>Password must contain at least 8 characters.</FieldError>
</Field>
);
export default FieldPasswordInputInvalidDemo;
Pin Input
Normal
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { PinInputRoot, PinInputSlots } from "@/components/ui/pin-input";
const FieldPinInputNormalDemo = () => (
<Field className="max-w-xs">
<FieldLabel>Verification code</FieldLabel>
<PinInputRoot count={6} type="numeric">
<PinInputSlots variant="grouped" separatorBetweenCount={3} />
</PinInputRoot>
<FieldDescription>
Enter the 6-digit code sent to your email.
</FieldDescription>
</Field>
);
export default FieldPinInputNormalDemo;
Invalid
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { PinInputRoot, PinInputSlots } from "@/components/ui/pin-input";
const FieldPinInputInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<FieldLabel>Verification code</FieldLabel>
<PinInputRoot count={6} type="numeric">
<PinInputSlots variant="grouped" separatorBetweenCount={3} />
</PinInputRoot>
<FieldError>Enter all 4 digits to continue.</FieldError>
</Field>
);
export default FieldPinInputInvalidDemo;
Radio Group
Normal
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const FieldRadioGroupNormalDemo = () => (
<Field className="max-w-xs">
<FieldLabel>Plan</FieldLabel>
<RadioGroup defaultValue="pro">
<Radio value="starter">Starter</Radio>
<Radio value="pro">Pro</Radio>
<Radio value="enterprise">Enterprise</Radio>
</RadioGroup>
<FieldDescription>
Select the billing plan that fits your needs.
</FieldDescription>
</Field>
);
export default FieldRadioGroupNormalDemo;
Invalid
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const FieldRadioGroupInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<FieldLabel>Billing cycle</FieldLabel>
<RadioGroup invalid>
<Radio value="monthly">Monthly</Radio>
<Radio value="yearly">Yearly</Radio>
</RadioGroup>
<FieldError>Please choose a billing cycle.</FieldError>
</Field>
);
export default FieldRadioGroupInvalidDemo;
Slider
Normal
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Slider, SliderField } from "@/components/ui/slider";
const FieldSliderNormalDemo = () => (
<Field className="max-w-xs">
<FieldLabel>Volume</FieldLabel>
<Slider aria-label={["Volume"]} defaultValue={60} min={0} max={100}>
<SliderField />
</Slider>
<FieldDescription>Use arrow keys for precise adjustments.</FieldDescription>
</Field>
);
export default FieldSliderNormalDemo;
Invalid
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Slider, SliderField } from "@/components/ui/slider";
const FieldSliderInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<FieldLabel>Daily usage limit</FieldLabel>
<Slider
aria-label={["Daily usage limit"]}
defaultValue={0}
invalid
min={10}
max={100}
>
<SliderField />
</Slider>
<FieldError>Set a value greater than or equal to 10.</FieldError>
</Field>
);
export default FieldSliderInvalidDemo;
Switch
Normal
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import { Switch, SwitchRoot } from "@/components/ui/switch";
const FieldSwitchNormalDemo = () => (
<Field className="max-w-xs">
<SwitchRoot defaultChecked>
<Switch />
<FieldLabel>Email notifications</FieldLabel>
</SwitchRoot>
<FieldDescription>Enable updates for account activity.</FieldDescription>
</Field>
);
export default FieldSwitchNormalDemo;
Invalid
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Switch, SwitchRoot } from "@/components/ui/switch";
const FieldSwitchInvalidDemo = () => (
<Field className="max-w-xs" invalid>
<SwitchRoot invalid>
<Switch />
<FieldLabel>Accept terms</FieldLabel>
</SwitchRoot>
<FieldError>You must accept the terms to continue.</FieldError>
</Field>
);
export default FieldSwitchInvalidDemo;
File Upload
Normal
Drop a PDF file or click to browse
import { FileTextIcon, XIcon } from "lucide-react";
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
} from "@/components/ui/file-upload";
const FieldFileUploadNormalDemo = () => (
<Field className="max-w-md">
<FieldLabel>Resume</FieldLabel>
<FileUpload accept={["application/pdf"]} maxFiles={1}>
<FileUploadDropzone className="min-h-24">
<FileTextIcon className="size-5 text-muted-foreground" />
<span className="text-muted-foreground text-xs">
Drop a PDF file or click to browse
</span>
</FileUploadDropzone>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem file={file} key={`${file.name}-${file.size}`}>
<FileUploadItemName />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
<FieldDescription>Supported format: PDF (max 1 file).</FieldDescription>
</Field>
);
export default FieldFileUploadNormalDemo;
Invalid
Only signed PDF files are accepted
import { FileTextIcon, XIcon } from "lucide-react";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import {
FileUpload,
FileUploadContext,
FileUploadDropzone,
FileUploadItem,
FileUploadItemDeleteTrigger,
FileUploadItemGroup,
FileUploadItemName,
} from "@/components/ui/file-upload";
const FieldFileUploadInvalidDemo = () => (
<Field className="max-w-md" invalid>
<FieldLabel>Verification document</FieldLabel>
<FileUpload accept={["application/pdf"]} invalid maxFiles={1}>
<FileUploadDropzone className="min-h-24">
<FileTextIcon className="size-5 text-muted-foreground" />
<span className="text-muted-foreground text-xs">
Only signed PDF files are accepted
</span>
</FileUploadDropzone>
<FileUploadItemGroup>
<FileUploadContext>
{({ acceptedFiles }) =>
acceptedFiles.map((file) => (
<FileUploadItem file={file} key={`${file.name}-${file.size}`}>
<FileUploadItemName />
<FileUploadItemDeleteTrigger aria-label={`Remove ${file.name}`}>
<XIcon />
</FileUploadItemDeleteTrigger>
</FileUploadItem>
))
}
</FileUploadContext>
</FileUploadItemGroup>
</FileUpload>
<FieldError>Please upload a signed PDF document.</FieldError>
</Field>
);
export default FieldFileUploadInvalidDemo;
API reference
This component mirrors the upstream Ark UI primitive.
See the ARK UI documentation for the full API.