Checkbox
Checked, unchecked or mixed. A hidden input sends the default value before the page hydrates, and a form reset brings it back.
client - size not measured yet
Checkbox
client - size not measured yetChecked, unchecked or mixed. A hidden input sends the default value before the page hydrates, and a form reset brings it back.
Try it
size
invalid
The code
checkbox.tsx
import { Checkbox } from 'topclassui/checkbox'
<div className="flex items-center gap-2">
<Checkbox id="checkbox-playground" defaultChecked />
<label htmlFor="checkbox-playground" className="text-sm text-body">I accept the terms</label>
</div>The box is a native <button role="checkbox">, and a hidden checkbox beside it is what the form sends.
Every state
9 statesImport from the package
topclassui/checkbox
import { Checkbox } from 'topclassui/checkbox'Install once:npm i topclassui@beta zod
Examples
- For a choice the form sends later. For a setting that takes effect at once, a Switch reads better.
- Unticked sends nothing and ticked sends on, before the page hydrates and after. An unchecked value can make an unticked box send a string of its own.
- A mixed box draws a dash and sets aria-checked to mixed; it is yours to clear, and it sends nothing while it is mixed.
- Checkbox draws no label of its own: write the label beside it, or around it, and give the box an
id. - In Ocean the
xsandsmboxes are circles: only the tick or the dash tells them from a radio.
basic
basic.tsx
import { Checkbox } from 'topclassui/checkbox'
/** Outside a Field: a native label points at the id of the checkbox, which is the button itself. */
export default function Demo() {
return (
<div className="grid justify-items-start gap-3">
<div className="flex items-center gap-2">
<Checkbox id="checkbox-basic-terms" defaultChecked />
<label htmlFor="checkbox-basic-terms" className="text-sm text-body">I accept the terms</label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="checkbox-basic-news" />
<label htmlFor="checkbox-basic-news" className="text-sm text-body">Send me product news</label>
</div>
</div>
)
}
labelled
labelled.tsx
import { Checkbox } from 'topclassui/checkbox'
/** The two native ways to name a box: a label that points at its id, and a label that wraps it. A click on either presses it. */
export default function Demo() {
return (
<div className="grid justify-items-start gap-3">
<div className="flex items-center gap-2">
<Checkbox id="checkbox-labelled-for" />
<label htmlFor="checkbox-labelled-for" className="text-sm text-body">Named by a label that points at it</label>
</div>
<label className="flex items-center gap-2 text-sm text-body">
<Checkbox id="checkbox-labelled-wrap" />
Named by a label that wraps it
</label>
</div>
)
}
form
form.tsx
import { Button } from 'topclassui/button'
import { Checkbox } from 'topclassui/checkbox'
/** A checked box and an unchecked one with uncheckedValue, in a plain form: terms=on and news=no, before any script runs and after. */
export default function Demo() {
return (
<form className="grid justify-items-start gap-3">
<div className="flex items-center gap-2">
<Checkbox id="checkbox-form-terms" name="terms" defaultChecked />
<label htmlFor="checkbox-form-terms" className="text-sm text-body">I accept the terms</label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="checkbox-form-news" name="news" value="yes" uncheckedValue="no" />
<label htmlFor="checkbox-form-news" className="text-sm text-body">Send me product news</label>
</div>
<Button type="submit">Save</Button>
</form>
)
}
sizes
sizes.tsx
import { Checkbox } from 'topclassui/checkbox'
/** The five sizes, checked. Each target stays 24 px, and 44 px under a finger, whatever the box. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-4">
<Checkbox size="xs" defaultChecked aria-label="Extra small" />
<Checkbox size="sm" defaultChecked aria-label="Small" />
<Checkbox size="md" defaultChecked aria-label="Medium" />
<Checkbox size="lg" defaultChecked aria-label="Large" />
<Checkbox size="xl" defaultChecked aria-label="Extra large" />
</div>
)
}
In Arabic, right to left
rtl.tsx
import { TopClassProvider } from 'topclassui/provider'
import { Checkbox } from 'topclassui/checkbox'
/** Arabic, inside TopClassProvider as the root layout would mount it. The box sits at the right, before its label. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar" className="grid justify-items-start gap-3">
<div className="flex items-center gap-2">
<Checkbox id="checkbox-rtl-terms" name="terms" defaultChecked />
<label htmlFor="checkbox-rtl-terms" className="text-sm text-body">أوافق على الشروط</label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="checkbox-rtl-news" name="news" />
<label htmlFor="checkbox-rtl-news" className="text-sm text-body">أرسل لي أخبار المنتج</label>
</div>
</div>
</TopClassProvider>
)
}
Props
Keyboard and accessibility
- A name is required:
idfor a<label htmlFor>, oraria-label, oraria-labelledby. A click on the label presses the box. - The target is 24px, 44px under a touch pointer, whatever
sizedraws: the focus ring is around the box. invalidsets aria-invalid, and the line of the box turns to danger and thickens, ticked or not.
Right to left
The box sits at the right, before its label, with no change to your code: the order is logical, not physical.
Server and client
A client component: it renders on the server and hydrates in the browser.
Files: index.tsx, client.tsx.
On the chip: client - size not measured yet.