Field
The label, the hint and the error around any form control, tied to it by ids in the server HTML before any script runs.
split - size not measured yet
Field
split - size not measured yetThe label, the hint and the error around any form control, tied to it by ids in the server HTML before any script runs.
The code
field.tsx
import { Input } from 'topclassui/input'
import { Field } from 'topclassui/field'
/** The label points at the input, and the input is named by it before any script runs. */
export default function Demo() {
return (
<div className="grid max-w-sm">
<Field name="email" label="Email">
{(control) => <Input {...control} type="email" autoComplete="email" fullWidth />}
</Field>
</div>
)
}
There is no Label component: the label is this prop, and the child spreads the control object onto the control.
Import from the package
topclassui/field
import { Field } from 'topclassui/field'Install once:npm i topclassui@beta zod
Examples
- In a server file use
Field; in a client fileFieldLiverenders the same HTML and shows the error the nearestFormActionreturned for itsname. - The child is a function,
(control) => <Input {...control} />. The control object isid,name,requiredand the three ARIA attributes, and each of the eleven controls takes it as it is. - The ids come from
idoruseId(), never fromname: two Fields may share a name. Passidwhen a page links to the field. erroris one message or the array Zod'sfieldErrorsgives; with one, the control is invalid unlessinvalidsays otherwise.inlineputs the control first and the label beside it, for a Checkbox or a Switch.
basic
basic.tsx
import { Input } from 'topclassui/input'
import { Field } from 'topclassui/field'
/** The label points at the input, and the input is named by it before any script runs. */
export default function Demo() {
return (
<div className="grid max-w-sm">
<Field name="email" label="Email">
{(control) => <Input {...control} type="email" autoComplete="email" fullWidth />}
</Field>
</div>
)
}
hint-error
Letters and numbers only
That name is taken
Shown on your profile
Too short
Add at least one sentence
Optional
hint-error.tsx
import { Input } from 'topclassui/input'
import { Textarea } from 'topclassui/textarea'
import { Field } from 'topclassui/field'
/** A hint, one error, and the array Zod's fieldErrors gives: the control reads the hint and then the errors. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<Field name="username" label="Username" hint="Letters and numbers only" error="That name is taken">
{(control) => <Input {...control} defaultValue="omar" autoComplete="username" fullWidth />}
</Field>
<Field name="bio" label="About you" hint="Shown on your profile" error={['Too short', 'Add at least one sentence']}>
{(control) => <Textarea {...control} defaultValue="Hi" fullWidth />}
</Field>
<Field name="website" label="Website" hint="Optional">
{(control) => <Input {...control} type="url" fullWidth />}
</Field>
</div>
)
}
required
We never share it
required.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Input } from 'topclassui/input'
import { Field } from 'topclassui/field'
/** The asterisk is only seen; the control says required itself: a native attribute, or aria-required on a Checkbox. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<Field name="email" label="Email" hint="We never share it" required>
{(control) => <Input {...control} type="email" autoComplete="email" fullWidth />}
</Field>
<Field name="terms" label="I accept the terms" required inline>
{(control) => <Checkbox {...control} />}
</Field>
</div>
)
}
inline
About once a month
Turn it on to get the summary
inline.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Switch } from 'topclassui/switch'
import { Field } from 'topclassui/field'
/** inline: the box first and the label beside it, the hint and the error under the label. A click on a label toggles its box. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<Field name="news" label="Send me product news" hint="About once a month" inline>
{(control) => <Checkbox {...control} />}
</Field>
<Field name="digest" label="Weekly digest" error="Turn it on to get the summary" inline>
{(control) => <Switch {...control} />}
</Field>
</div>
)
}
group
You can change it later
group.tsx
import { RadioGroupItem, RadioGroupRoot } from 'topclassui/radio-group'
import { Field } from 'topclassui/field'
/** A group no `<label for>` can name: the control object's aria-labelledby names it, and its hint describes it. */
export default function Demo() {
return (
<div className="grid max-w-sm">
<Field name="plan" label="Plan" hint="You can change it later" required>
{(control) => (
<RadioGroupRoot {...control} defaultValue="pro">
<RadioGroupItem value="free">Free</RadioGroupItem>
<RadioGroupItem value="pro">Pro</RadioGroupItem>
<RadioGroupItem value="team">Team</RadioGroupItem>
</RadioGroupRoot>
)}
</Field>
</div>
)
}
controls
controls.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Combobox } from 'topclassui/combobox'
import { Input } from 'topclassui/input'
import { NumberField } from 'topclassui/number-field'
import { OTPField } from 'topclassui/otp-field'
import { RadioGroupItem, RadioGroupRoot } from 'topclassui/radio-group'
import { SegmentedControlItem, SegmentedControlRoot } from 'topclassui/segmented-control'
import { Select } from 'topclassui/select'
import { Slider } from 'topclassui/slider'
import { Switch } from 'topclassui/switch'
import { Textarea } from 'topclassui/textarea'
import { Field } from 'topclassui/field'
const COUNTRIES = [{ value: 'jo', label: 'Jordan' }, { value: 'eg', label: 'Egypt' }, { value: 'ma', label: 'Morocco' }]
/** The eleven controls, each spreading one control object as it is: each is named by the label, described by the hint
* and then the error, and invalid, in the server HTML and once hydrated. */
export default function Demo() {
return (
<form className="grid max-w-sm gap-5">
<Field name="email" label="Email" hint="Input hint" error="Input error" required>
{(control) => <Input {...control} type="email" fullWidth />}
</Field>
<Field name="bio" label="Bio" hint="Textarea hint" error="Textarea error">
{(control) => <Textarea {...control} fullWidth />}
</Field>
<Field name="terms" label="Terms" hint="Checkbox hint" error="Checkbox error" required inline>
{(control) => <Checkbox {...control} />}
</Field>
<Field name="digest" label="Digest" hint="Switch hint" error="Switch error" inline>
{(control) => <Switch {...control} />}
</Field>
<Field name="plan" label="Plan" hint="RadioGroup hint" error="RadioGroup error" required>
{(control) => (
<RadioGroupRoot {...control} orientation="horizontal">
<RadioGroupItem value="free">Free</RadioGroupItem>
<RadioGroupItem value="pro">Pro</RadioGroupItem>
</RadioGroupRoot>
)}
</Field>
<Field name="view" label="View" hint="SegmentedControl hint" error="SegmentedControl error">
{(control) => (
<SegmentedControlRoot {...control} defaultValue="board">
<SegmentedControlItem value="board">Board</SegmentedControlItem>
<SegmentedControlItem value="table">Table</SegmentedControlItem>
</SegmentedControlRoot>
)}
</Field>
<Field name="volume" label="Volume" hint="Slider hint" error="Slider error">
{(control) => <Slider {...control} defaultValue={40} />}
</Field>
<Field name="guests" label="Guests" hint="NumberField hint" error="NumberField error">
{(control) => <NumberField {...control} defaultValue={2} min={1} max={8} />}
</Field>
<Field name="code" label="Code" hint="OTPField hint" error="OTPField error">
{(control) => <OTPField {...control} length={4} />}
</Field>
<Field name="country" label="Country" hint="Select hint" error="Select error" required>
{(control) => <Select {...control} items={COUNTRIES} defaultValue="jo" />}
</Field>
<Field name="city" label="City" hint="Combobox hint" error="Combobox error">
{(control) => <Combobox {...control} items={COUNTRIES} />}
</Field>
</form>
)
}
form
form.tsx
import { Button } from 'topclassui/button'
import { Checkbox } from 'topclassui/checkbox'
import { Input } from 'topclassui/input'
import { Textarea } from 'topclassui/textarea'
import { Field } from 'topclassui/field'
/** A plain form: Field adds nothing it sends, the controls do, before any script runs and after. */
export default function Demo() {
return (
<form className="grid max-w-sm gap-4">
<Field name="email" label="Email" hint="We never share it" required>
{(control) => <Input {...control} type="email" defaultValue="omar@example.com" autoComplete="email" fullWidth />}
</Field>
<Field name="message" label="Message">
{(control) => <Textarea {...control} defaultValue="Hello" fullWidth />}
</Field>
<Field name="terms" label="I accept the terms" inline>
{(control) => <Checkbox {...control} defaultChecked />}
</Field>
<div className="flex flex-wrap gap-2">
<Button type="submit">Send</Button>
<Button type="reset" variant="outline">Clear</Button>
</div>
</form>
)
}
live
Shown to other players
Pick the size you ordered
live.tsx
'use client'
import { Input } from 'topclassui/input'
import { Select } from 'topclassui/select'
import { FieldLive } from 'topclassui/field'
const SIZES = [{ value: 's', label: 'Small' }, { value: 'm', label: 'Medium' }, { value: 'l', label: 'Large' }]
/** FieldLive in a client file, outside a form: the same server HTML as Field, and an error of its own stays until the caller drops it. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<FieldLive name="nickname" label="Nickname" hint="Shown to other players">
{(control) => <Input {...control} autoComplete="nickname" fullWidth />}
</FieldLive>
<FieldLive name="size" label="Size" error="Pick the size you ordered" required>
{(control) => <Select {...control} items={SIZES} defaultValue="m" />}
</FieldLive>
</div>
)
}
long-content
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
long-content.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Input } from 'topclassui/input'
import { Field } from 'topclassui/field'
const WORD = 'x'.repeat(300)
const SENTENCE = 'A label long enough to wrap onto several lines on any phone, '.repeat(5)
/** Hostile text: a 300-character word in the label, the hint and the error, and a long sentence beside a box. It wraps; nothing scrolls. */
export default function Demo() {
return (
<div className="grid max-w-sm gap-4">
<Field name="word" label={WORD} hint={WORD} error={WORD} required>
{(control) => <Input {...control} fullWidth />}
</Field>
<Field name="sentence" label={SENTENCE} hint={WORD} inline>
{(control) => <Checkbox {...control} />}
</Field>
</div>
)
}
In Arabic, right to left
لن نشاركه مع أحد
هذا البريد مستخدم
اقرأها قبل المتابعة
rtl.tsx
import { Checkbox } from 'topclassui/checkbox'
import { Input } from 'topclassui/input'
import { TopClassProvider } from 'topclassui/provider'
import { Field } from 'topclassui/field'
/** Arabic, inside TopClassProvider as the root layout would mount it: the asterisk follows the label, and an inline box sits at the right. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar" className="grid max-w-sm gap-4">
<Field name="email" label="البريد الإلكتروني" hint="لن نشاركه مع أحد" error="هذا البريد مستخدم" required>
{(control) => <Input {...control} type="email" defaultValue="omar@example.com" fullWidth />}
</Field>
<Field name="terms" label="أوافق على الشروط" hint="اقرأها قبل المتابعة" inline>
{(control) => <Checkbox {...control} />}
</Field>
</div>
</TopClassProvider>
)
}
Props
Keyboard and accessibility
- The label names the control through
<label for>andaria-labelledby;aria-describedbyreads the hint and then the error, only those rendered. - The asterisk is only seen:
requiredreaches the control, which says so itself, asrequiredoraria-required. - The error is no live region. It arrives with the page, and after a FormAction the focus goes to the first invalid control.
Right to left
The label, the hint and the error start at the right; the asterisk follows the label, and an inline box sits at the right.
Server and client
Renders on the server; the interactive part is a client component that ships only when it is used.
Files: index.tsx, client.tsx.
On the chip: split - size not measured yet.