Field
A labelled form control with a description and validation errors, plus a form that checks them all.
'use client'
import * as React from 'react'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'
import { Field, FieldControl, FieldDescription, FieldError, FieldForm, FieldGroup, FieldLabel } from '@/components/ui/field'
const roles = [
{ value: 'designer', label: 'Designer' },
{ value: 'engineer', label: 'Engineer' },
{ value: 'product', label: 'Product manager' },
]
/** Client-side validation with no form library: Field checks each control, FieldForm checks them all on submit. */
export default function FieldFormExample() {
const [saved, setSaved] = React.useState<string | null>(null)
return (
<FieldForm
className="max-w-sm"
onFormSubmit={values => setSaved(`Welcome aboard, ${String(values.name).trim()}!`)}
>
<FieldGroup>
<Field
name="name"
validate={value => (String(value ?? '').trim().length < 2 ? 'Enter at least 2 characters.' : null)}
>
<FieldLabel>Full name</FieldLabel>
<FieldControl required placeholder="Ada Lovelace" />
<FieldDescription>As it should appear on your profile.</FieldDescription>
<FieldError />
</Field>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<FieldControl type="email" required placeholder="ada@example.com" />
<FieldError match="valueMissing">Email is required.</FieldError>
<FieldError match="typeMismatch">That doesn't look like an email address.</FieldError>
</Field>
<Field name="role">
<FieldLabel nativeLabel={false} render={<div />}>
Role
</FieldLabel>
<Select name="role" required>
<SelectTrigger className="w-full">
<SelectValue placeholder="Choose a role" />
</SelectTrigger>
<SelectContent>
{roles.map(role => (
<SelectItem key={role.value} value={role.value}>
{role.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FieldError match="valueMissing">Pick the role closest to yours.</FieldError>
</Field>
<Field name="terms">
<FieldLabel className="font-normal">
<Checkbox required />
I agree to the terms of service
</FieldLabel>
<FieldError match="valueMissing">You need to accept the terms to continue.</FieldError>
</Field>
</FieldGroup>
<div className="flex items-center gap-3">
<Button type="submit">Create account</Button>
{saved && (
<p role="status" className="text-sm text-muted-foreground">
{saved}
</p>
)}
</div>
</FieldForm>
)
}
Built on Base UI
Installation
pnpm dlx shadcn@latest add https://kagadmodyaa.vercel.app/r/field.jsonNeeds a React + Tailwind project set up with shadcn. Copies the component source straight into your project, so you can edit it.
Field imports cn() from lib/utils.ts — add it too if you don't have it yet:
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Usage
import { Field, FieldLabel, FieldControl, FieldDescription, FieldError, FieldForm } from '@/components/ui/field'
import { Button } from '@/components/ui/button'
<FieldForm onFormSubmit={values => console.log(values)}>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<FieldControl type="email" required />
<FieldDescription>We only use it to send receipts.</FieldDescription>
<FieldError match="valueMissing">Email is required.</FieldError>
<FieldError match="typeMismatch">That doesn't look like an email.</FieldError>
</Field>
<Button type="submit">Subscribe</Button>
</FieldForm>
// Checkbox and Select work inside a Field too — wrap them in a FieldLabel:
<Field name="terms">
<FieldLabel><Checkbox required /> I agree</FieldLabel>
<FieldError match="valueMissing">Please accept the terms.</FieldError>
</Field>Examples
States and fieldset
Pick something you can live with — it shows in your profile URL.
Enter a full email address, like ada@example.com.
Only an owner can rename the workspace.
import { Checkbox } from '@/components/ui/checkbox'
import {
Field,
FieldControl,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
} from '@/components/ui/field'
/** The states a Field can be in, and a FieldSet grouping related options under a legend. */
export default function FieldStates() {
return (
<FieldGroup className="max-w-sm">
<Field>
<FieldLabel>Username</FieldLabel>
<FieldControl defaultValue="kagad" />
<FieldDescription>Pick something you can live with — it shows in your profile URL.</FieldDescription>
</Field>
<Field invalid>
<FieldLabel>Email</FieldLabel>
<FieldControl type="email" defaultValue="ada@" />
<FieldError match>Enter a full email address, like ada@example.com.</FieldError>
</Field>
<Field disabled>
<FieldLabel>Workspace</FieldLabel>
<FieldControl defaultValue="Kagadmodyaa Studio" />
<FieldDescription>Only an owner can rename the workspace.</FieldDescription>
</Field>
<FieldSet>
<FieldLegend>Notify me about</FieldLegend>
<Field name="comments">
<FieldLabel className="font-normal">
<Checkbox defaultChecked />
Comments on my work
</FieldLabel>
</Field>
<Field name="releases">
<FieldLabel className="font-normal">
<Checkbox />
New releases
</FieldLabel>
</Field>
</FieldSet>
</FieldGroup>
)
}
API Reference
| Name | Type | Default | Description |
|---|---|---|---|
| Field name | string | — | Identifies the field in the submitted values and in server-provided errors. |
| Field validate | (value, formValues) => string | string[] | null | — | Custom check. Return a message (or several) when the value is invalid, or null when it is fine. |
| Field validationMode | 'onSubmit' | 'onBlur' | 'onChange' | 'onSubmit' | When to validate first. After a failed submit the field re-checks as the value changes, so errors clear when fixed. |
| Field invalid / disabled | boolean | — | Force the invalid or disabled state, e.g. for an error that came from the server. |
| FieldError match | boolean | keyof ValidityState | — | Show this message for one kind of failure ('valueMissing', 'typeMismatch', …), or always with true. Omit it to show the browser's or validate's own message. |
| FieldControl | input props | — | Our Input, wired to the label, description and error for accessibility. Accepts every native input attribute. |
| FieldForm errors | Record<string, string | string[]> | — | Server errors by field name; shown on the matching Field until it changes. |
| FieldForm onFormSubmit | (values, details) => void | — | Called with the form values once every field is valid. |
| FieldSet / FieldLegend | component | — | A fieldset with a legend, for groups of related fields. |