Field composition primitives for labels and messages
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
...import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
const years = ['2024', '2025', '2026', '2027', '2028', '2029']
export default function FieldDemoExample() {
return (
<form class="w-full max-w-md"><FieldGroup><FieldSet><FieldLegend>Payment Method</FieldLegend><FieldDescription>All transactions are secure and encrypted</FieldDescription><FieldGroup><Field><FieldLabel for="card-name">Name on Card</FieldLabel><Input id="card-name" placeholder="Evil Rabbit" /></Field><Field><FieldLabel for="card-number">Card Number</FieldLabel><Input id="card-number" placeholder="1234 5678 9012 3456" /><FieldDescription>Enter your 16-digit card number</FieldDescription></Field><div class="grid grid-cols-3 gap-3"><Field><FieldLabel>Month</FieldLabel><Select><SelectTrigger aria-label="MM"><SelectValue placeholder="MM" /></SelectTrigger><SelectContent><SelectGroup>{months.map(month => <SelectItem value={month}>{month}</SelectItem>)}</SelectGroup></SelectContent></Select></Field><Field><FieldLabel>Year</FieldLabel><Select><SelectTrigger aria-label="YYYY"><SelectValue placeholder="YYYY" /></SelectTrigger><SelectContent><SelectGroup>{years.map(year => <SelectItem value={year}>{year}</SelectItem>)}</SelectGroup></SelectContent></Select></Field><Field><FieldLabel for="cvv">CVV</FieldLabel><Input id="cvv" placeholder="123" /></Field></div></FieldGroup></FieldSet><FieldSeparator /><FieldSet><FieldLegend>Billing Address</FieldLegend><FieldDescription>The billing address associated with your payment method</FieldDescription><Field orientation="horizontal"><Checkbox id="shipping-address" defaultChecked /><FieldLabel for="shipping-address">Same as shipping address</FieldLabel></Field></FieldSet><Field><FieldLabel for="comments">Comments</FieldLabel><Textarea id="comments" placeholder="Add any additional comments" /></Field><Field orientation="horizontal"><Button type="submit">Submit</Button><Button type="button" variant="outline">Cancel</Button></Field></FieldGroup></form>
)
}
Installation
npx @fictjs/shadcn@latest add fieldCopy the Fict shadcn component into your project.
Source
components/ui/field.tsx
src/components/ui/field.tsx
import { cn } from '@/lib/cn'
import { Label } from '@/components/ui/label'
type DivProps = JSX.IntrinsicElements['div']
type ParagraphProps = JSX.IntrinsicElements['p']
type FieldsetProps = JSX.IntrinsicElements['fieldset']
type LegendProps = JSX.IntrinsicElements['legend']
export function Field(props: DivProps & { orientation?: 'vertical' | 'horizontal' | 'responsive' }) {
const { class: className, orientation = 'vertical', ...rest } = props
return <div data-slot='field' data-orientation={orientation} class={cn('grid gap-2', orientation === 'horizontal' && 'grid-cols-[auto_1fr] items-center', orientation === 'responsive' && 'grid-cols-1 sm:grid-cols-[1fr_2fr] sm:items-center', className)} {...rest} />
}
export function FieldLabel(props: JSX.IntrinsicElements['label']) {
return <Label {...props} />
}
export function FieldControl(props: DivProps) {
const { class: className, ...rest } = props
return <div data-slot='field-control' class={cn('grid gap-1', className)} {...rest} />
}
export function FieldDescription(props: ParagraphProps) {
const { class: className, ...rest } = props
return <p class={cn('text-xs text-muted-foreground', className)} {...rest} />
}
export function FieldError(props: ParagraphProps) {
const { class: className, ...rest } = props
return <p class={cn('text-xs font-medium text-destructive', className)} {...rest} />
}
export function FieldSet(props: FieldsetProps) {
const { class: className, ...rest } = props
return <fieldset data-slot='field-set' class={cn('grid gap-4', className)} {...rest} />
}
export function FieldLegend(props: LegendProps & { variant?: 'legend' | 'label' }) {
const { class: className, variant = 'legend', ...rest } = props
return <legend data-slot='field-legend' data-variant={variant} class={cn(variant === 'legend' ? 'text-base font-semibold' : 'text-sm font-medium', className)} {...rest} />
}
export function FieldGroup(props: DivProps) {
const { class: className, ...rest } = props
return <div data-slot='field-group' class={cn('grid gap-4', className)} {...rest} />
}
export function FieldContent(props: DivProps) {
const { class: className, ...rest } = props
return <div data-slot='field-content' class={cn('grid gap-1', className)} {...rest} />
}
export function FieldSeparator(props: DivProps) {
const { class: className, children, ...rest } = props
return <div role='separator' data-slot='field-separator' class={cn('relative my-2 h-px bg-border', className)} {...rest}>{children}</div>
}
Usage
Examples
Input
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
...import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
export default function FieldInputExample() {
return (
<FieldSet class="w-80">
<FieldGroup>
<Field><FieldLabel for="username">Username</FieldLabel><Input id="username" placeholder="Max Leiter" /><FieldDescription>Choose a unique username for your account.</FieldDescription></Field>
<Field><FieldLabel for="password">Password</FieldLabel><FieldDescription>Must be at least 8 characters long.</FieldDescription><Input id="password" type="password" placeholder="••••••••" /></Field>
</FieldGroup>
</FieldSet>
)
}
Textarea
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field'
import { Textarea } from '@/components/ui/textarea'
...import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from '@/components/ui/field'
import { Textarea } from '@/components/ui/textarea'
export default function FieldTextareaExample() {
return (
<FieldSet class="w-80"><FieldGroup><Field><FieldLabel for="feedback">Feedback</FieldLabel><Textarea id="feedback" placeholder="Your feedback helps us improve..." rows={4} /><FieldDescription>Share your thoughts about our service.</FieldDescription></Field></FieldGroup></FieldSet>
)
}
Select
Select your department or area of work.
import { Field, FieldDescription, FieldLabel } from '@/components/ui/field'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
...import { Field, FieldDescription, FieldLabel } from '@/components/ui/field'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
const departments = ['Engineering', 'Design', 'Marketing', 'Sales', 'Customer Support', 'Human Resources', 'Finance', 'Operations']
export default function FieldSelectExample() {
return (
<Field class="w-80"><FieldLabel for="department">Department</FieldLabel><Select><SelectTrigger id="department"><SelectValue placeholder="Choose department" /></SelectTrigger><SelectContent>{departments.map(department => <SelectItem value={department.toLowerCase().replaceAll(' ', '-')}>{department}</SelectItem>)}</SelectContent></Select><FieldDescription>Select your department or area of work.</FieldDescription></Field>
)
}
Slider
import { Field, FieldDescription, FieldLabel } from '@/components/ui/field'
import { Slider } from '@/components/ui/slider'
...import { Field, FieldDescription, FieldLabel } from '@/components/ui/field'
import { Slider } from '@/components/ui/slider'
export default function FieldSliderExample() {
let value = $state([200, 800])
return (
<Field class="w-80"><FieldLabel>Price Range</FieldLabel><FieldDescription>Set your budget range (${value[0]} - ${value[1]}).</FieldDescription><Slider value={() => value} onValueChange={next => value = next} min={0} max={1000} step={10} aria-label="Price Range" /></Field>
)
}
Fieldset
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
...import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
export default function FieldFieldsetExample() {
return (
<FieldSet class="w-96"><FieldLegend>Address Information</FieldLegend><FieldDescription>We need your address to deliver your order.</FieldDescription><FieldGroup><Field><FieldLabel for="street">Street Address</FieldLabel><Input id="street" placeholder="123 Main St" /></Field><div class="grid grid-cols-2 gap-4"><Field><FieldLabel for="city">City</FieldLabel><Input id="city" placeholder="New York" /></Field><Field><FieldLabel for="postal-code">Postal Code</FieldLabel><Input id="postal-code" placeholder="90502" /></Field></div></FieldGroup></FieldSet>
)
}
Checkbox
Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.
import { Checkbox } from '@/components/ui/checkbox'
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from '@/components/ui/field'
...import { Checkbox } from '@/components/ui/checkbox'
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from '@/components/ui/field'
const desktopItems = ['Hard disks', 'External disks', 'CDs, DVDs, and iPods', 'Connected servers']
export default function FieldCheckboxExample() {
return (
<FieldGroup class="w-80"><FieldSet><FieldLegend variant="label">Show these items on the desktop</FieldLegend><FieldDescription>Select the items you want to show on the desktop.</FieldDescription><FieldGroup>{desktopItems.map((label, index) => <Field orientation="horizontal"><Checkbox id={`desktop-${index}`} /><FieldLabel for={`desktop-${index}`}>{label}</FieldLabel></Field>)}</FieldGroup></FieldSet><FieldSeparator /><Field orientation="horizontal"><Checkbox id="sync-folders" defaultChecked /><FieldContent><FieldLabel for="sync-folders">Sync Desktop & Documents folders</FieldLabel><FieldDescription>Your Desktop & Documents folders are being synced with iCloud Drive. You can access them from other devices.</FieldDescription></FieldContent></Field></FieldGroup>
)
}
Radio
import { Field, FieldDescription, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
...import { Field, FieldDescription, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
const plans = [['monthly', 'Monthly ($9.99/month)'], ['yearly', 'Yearly ($99.99/year)'], ['lifetime', 'Lifetime ($299.99)']] as const
export default function FieldRadioExample() {
return (
<FieldSet class="w-80"><FieldLegend variant="label">Subscription Plan</FieldLegend><FieldDescription>Yearly and lifetime plans offer significant savings.</FieldDescription><RadioGroup defaultValue="monthly">{plans.map(([value, label]) => <Field orientation="horizontal"><RadioGroupItem id={`plan-${value}`} value={value} /><FieldLabel for={`plan-${value}`}>{label}</FieldLabel></Field>)}</RadioGroup></FieldSet>
)
}
Switch
import { Field, FieldLabel } from '@/components/ui/field'
import { Switch } from '@/components/ui/switch'
...import { Field, FieldLabel } from '@/components/ui/field'
import { Switch } from '@/components/ui/switch'
export default function FieldSwitchExample() {
return (
<Field orientation="horizontal" class="w-fit"><FieldLabel for="multi-factor">Multi-factor authentication</FieldLabel><Switch id="multi-factor" /></Field>
)
}
Choice Card
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
...import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
const environments = [['kubernetes', 'Kubernetes', 'Run GPU workloads on a K8s cluster.'], ['vm', 'Virtual Machine', 'Access a cluster to run GPU workloads.']] as const
export default function FieldChoiceCardExample() {
return (
<FieldSet class="w-80"><FieldLegend variant="label">Compute Environment</FieldLegend><FieldDescription>Select the compute environment for your cluster.</FieldDescription><RadioGroup defaultValue="kubernetes"><FieldGroup>{environments.map(([value, label, description]) => <FieldLabel class="rounded-lg border p-3" for={`environment-${value}`}><Field orientation="horizontal"><FieldContent><span class="font-medium">{label}</span><FieldDescription>{description}</FieldDescription></FieldContent><RadioGroupItem id={`environment-${value}`} value={value} /></Field></FieldLabel>)}</FieldGroup></RadioGroup></FieldSet>
)
}
Group
import { Checkbox } from '@/components/ui/checkbox'
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldSeparator, FieldSet } from '@/components/ui/field'
...import { Checkbox } from '@/components/ui/checkbox'
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldSeparator, FieldSet } from '@/components/ui/field'
export default function FieldGroupExample() {
return (
<FieldGroup class="w-80"><FieldSet><FieldLabel>Responses</FieldLabel><FieldDescription>Get notified when ChatGPT responds to requests that take time, like research or image generation.</FieldDescription><Field orientation="horizontal"><Checkbox id="response-push" defaultChecked disabled /><FieldLabel for="response-push">Push notifications</FieldLabel></Field></FieldSet><FieldSeparator /><FieldSet><FieldContent><FieldLabel>Tasks</FieldLabel><FieldDescription>Get notified when tasks you've created have updates. <a href="#">Manage tasks</a></FieldDescription></FieldContent><Field orientation="horizontal"><Checkbox id="task-push" /><FieldLabel for="task-push">Push notifications</FieldLabel></Field><Field orientation="horizontal"><Checkbox id="task-email" /><FieldLabel for="task-email">Email notifications</FieldLabel></Field></FieldSet></FieldGroup>
)
}
Rtl
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
...import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSeparator, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
const months = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
const years = ['2024', '2025', '2026', '2027', '2028', '2029']
const translations = {
ar: { dir: 'rtl', payment: 'طريقة الدفع', secure: 'جميع المعاملات آمنة ومشفرة', name: 'الاسم على البطاقة', cardNumber: 'رقم البطاقة', cardNumberDescription: 'أدخل رقم البطاقة المكون من 16 رقمًا', month: 'الشهر', year: 'السنة', billing: 'عنوان الفوترة', billingDescription: 'عنوان الفوترة المرتبط بطريقة الدفع الخاصة بك', shipping: 'نفس عنوان الشحن', comments: 'تعليقات', commentsPlaceholder: 'أضف أي تعليقات إضافية', submit: 'إرسال', cancel: 'إلغاء' },
he: { dir: 'rtl', payment: 'אמצעי תשלום', secure: 'כל העסקאות מאובטחות ומוצפנות', name: 'שם על הכרטיס', cardNumber: 'מספר כרטיס', cardNumberDescription: 'הזן את מספר הכרטיס בן 16 הספרות שלך', month: 'חודש', year: 'שנה', billing: 'כתובת חיוב', billingDescription: 'כתובת החיוב המשויכת לאמצעי התשלום שלך', shipping: 'זהה לכתובת המשלוח', comments: 'הערות', commentsPlaceholder: 'הוסף הערות נוספות', submit: 'שלח', cancel: 'בטל' },
en: { dir: 'ltr', payment: 'Payment Method', secure: 'All transactions are secure and encrypted', name: 'Name on Card', cardNumber: 'Card Number', cardNumberDescription: 'Enter your 16-digit card number', month: 'Month', year: 'Year', billing: 'Billing Address', billingDescription: 'The billing address associated with your payment method', shipping: 'Same as shipping address', comments: 'Comments', commentsPlaceholder: 'Add any additional comments', submit: 'Submit', cancel: 'Cancel' },
} as const
export default function FieldRtlExample() {
let language = $state<keyof typeof translations>('ar')
const t = translations[language]
const monthLabel = (month: string) => language === 'ar' ? new Intl.NumberFormat('ar-SA', { useGrouping: false }).format(Number(month)) : month
return (
<div><select aria-label="Preview language" value={language} onChange={event => language = event.currentTarget.value as keyof typeof translations}><option value="ar">Arabic (العربية)</option><option value="he">Hebrew (עברית)</option><option value="en">English</option></select><form class="w-full max-w-md" dir={t.dir}><FieldGroup><FieldSet><FieldLegend>{t.payment}</FieldLegend><FieldDescription>{t.secure}</FieldDescription><FieldGroup><Field><FieldLabel for="card-name-rtl">{t.name}</FieldLabel><Input id="card-name-rtl" placeholder="Evil Rabbit" required /></Field><Field><FieldLabel for="card-number-rtl">{t.cardNumber}</FieldLabel><Input id="card-number-rtl" placeholder="1234 5678 9012 3456" required /><FieldDescription>{t.cardNumberDescription}</FieldDescription></Field><div class="grid grid-cols-3 gap-4"><Field><FieldLabel for="month-rtl">{t.month}</FieldLabel><Select><SelectTrigger id="month-rtl"><SelectValue placeholder="MM" /></SelectTrigger><SelectContent dir={t.dir}><SelectGroup>{months.map(month => <SelectItem value={month}>{monthLabel(month)}</SelectItem>)}</SelectGroup></SelectContent></Select></Field><Field><FieldLabel for="year-rtl">{t.year}</FieldLabel><Select><SelectTrigger id="year-rtl"><SelectValue placeholder="YYYY" /></SelectTrigger><SelectContent dir={t.dir}><SelectGroup>{years.map(year => <SelectItem value={year}>{year}</SelectItem>)}</SelectGroup></SelectContent></Select></Field><Field><FieldLabel for="cvv-rtl">CVV</FieldLabel><Input id="cvv-rtl" placeholder="123" required /></Field></div></FieldGroup></FieldSet><FieldSeparator /><FieldSet><FieldLegend>{t.billing}</FieldLegend><FieldDescription>{t.billingDescription}</FieldDescription><FieldGroup><Field orientation="horizontal"><Checkbox id="shipping-rtl" defaultChecked /><FieldLabel for="shipping-rtl" class="font-normal">{t.shipping}</FieldLabel></Field></FieldGroup></FieldSet><FieldSet><FieldGroup><Field><FieldLabel for="comments-rtl">{t.comments}</FieldLabel><Textarea id="comments-rtl" placeholder={t.commentsPlaceholder} class="resize-none" /></Field></FieldGroup></FieldSet><Field orientation="horizontal"><Button type="submit">{t.submit}</Button><Button type="button" variant="outline">{t.cancel}</Button></Field></FieldGroup></form></div>
)
}
Responsive
import { Button } from '@/components/ui/button'
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
...import { Button } from '@/components/ui/button'
import { Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet } from '@/components/ui/field'
import { Input } from '@/components/ui/input'
export default function FieldResponsiveExample() {
return (
<form class="w-full max-w-lg"><FieldSet><FieldLegend>Profile</FieldLegend><FieldDescription>Fill in your profile information.</FieldDescription><FieldGroup><Field orientation="responsive"><FieldContent><FieldLabel for="profile-name">Name</FieldLabel><FieldDescription>Provide your full name for identification</FieldDescription></FieldContent><Input id="profile-name" placeholder="Evil Rabbit" required /></Field><Field orientation="responsive"><span /><div class="flex gap-2"><Button type="submit">Submit</Button><Button type="button" variant="outline">Cancel</Button></div></Field></FieldGroup></FieldSet></form>
)
}