Primitives
Checkbox
A control for an on/off choice that applies on submit.
import { Checkbox, Label } from 'ferry-ui'
const EVENTS = [
{ value: 'invoice-paid', label: 'A customer pays an invoice', checked: true },
{ value: 'payment-failed', label: 'A payment fails', checked: true },
{ value: 'member-joined', label: 'A member joins the workspace', checked: false },
]
export default function CheckboxHero() {
return (
<fieldset className="flex flex-col gap-3">
<legend className="mb-3 text-sm font-medium text-foreground">Send me an email when</legend>
{EVENTS.map((event) => (
<Label key={event.value} className="font-normal">
<Checkbox defaultChecked={event.checked} />
{event.label}
</Label>
))}
</fieldset>
)
}aria-label.| Choice | Control |
|---|---|
| A choice that applies on submit, or more than one value | Checkbox |
| A setting that applies immediately | Switch |
| One value among 2 to 5 options | Radio Group |
| A button that stays pressed in a toolbar | Toggle |
Import the component. A checkbox has one part.
import { Checkbox } from 'ferry-ui'
<Checkbox />A Label around the checkbox makes the text a part of the click area.
To add a description, link the label with htmlFor. Link the description with aria-describedby.
Each Monday, the report goes to all the admins of the workspace.
import { Checkbox, Label } from 'ferry-ui'
export default function CheckboxDescription() {
return (
<div className="flex max-w-sm items-start gap-2.5">
<Checkbox id="weekly-report" aria-describedby="weekly-report-hint" className="mt-0.5" />
<div className="flex flex-col gap-0.5">
<Label htmlFor="weekly-report">Email me a weekly report</Label>
<p id="weekly-report-hint" className="text-[13px] text-foreground-light">
Each Monday, the report goes to all the admins of the workspace.
</p>
</div>
</div>
)
}defaultChecked sets the first state. The value 'indeterminate' shows a dash. disabled stops all clicks.
import { Checkbox, Label } from 'ferry-ui'
export default function CheckboxStates() {
return (
<div className="flex flex-col gap-3">
<Label>
<Checkbox />
Unchecked
</Label>
<Label>
<Checkbox defaultChecked />
Checked
</Label>
<Label>
<Checkbox defaultChecked="indeterminate" />
Indeterminate
</Label>
<Label>
<Checkbox disabled />
Disabled
</Label>
<Label>
<Checkbox disabled defaultChecked />
Disabled and checked
</Label>
</div>
)
}A checkbox holds its state by default. To control the state, pass checked and onCheckedChange.
onCheckedChange gets true, false or 'indeterminate'.
import * as React from 'react'
import { Checkbox, Label, Textarea } from 'ferry-ui'
export default function CheckboxControlled() {
const [withNote, setWithNote] = React.useState(false)
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<Label>
<Checkbox checked={withNote} onCheckedChange={(checked) => setWithNote(checked === true)} />
Add a note to the invoice
</Label>
{withNote && <Textarea aria-label="Note" placeholder="Thank you for your order." />}
</div>
)
}Use 'indeterminate' when the user selects only some items of a list. A click on an indeterminate checkbox sends true.
import * as React from 'react'
import { Checkbox, Label } from 'ferry-ui'
const MEMBERS = [
{ id: 'maya', name: 'Maya Chen' },
{ id: 'sam', name: 'Sam Lee' },
{ id: 'nora', name: 'Nora Diaz' },
]
export default function CheckboxSelectAll() {
const [selected, setSelected] = React.useState(['maya'])
const all = selected.length === MEMBERS.length
const some = selected.length > 0 && !all
function toggle(id: string, checked: boolean) {
setSelected((current) => (checked ? [...current, id] : current.filter((item) => item !== id)))
}
return (
<div className="flex w-full max-w-xs flex-col divide-y rounded-lg border bg-surface-100">
<Label className="px-4 py-2.5">
<Checkbox
checked={all ? true : some ? 'indeterminate' : false}
onCheckedChange={(checked) => setSelected(checked === true ? MEMBERS.map((member) => member.id) : [])}
/>
All members
</Label>
{MEMBERS.map((member) => (
<Label key={member.id} className="px-4 py-2.5 font-normal">
<Checkbox
checked={selected.includes(member.id)}
onCheckedChange={(checked) => toggle(member.id, checked === true)}
/>
{member.name}
</Label>
))}
</div>
)
}In a <form>, name and value add the checkbox to the data of the form. required makes the checkbox mandatory.
import * as React from 'react'
import { Button, Checkbox, Label } from 'ferry-ui'
const PERMISSIONS = [
{ value: 'read', label: 'Read projects' },
{ value: 'write', label: 'Create and edit projects' },
{ value: 'billing', label: 'Manage billing' },
]
export default function CheckboxForm() {
const [sent, setSent] = React.useState<string>()
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
// The form holds one "permissions" entry for each checked checkbox.
const values = new FormData(event.currentTarget).getAll('permissions')
setSent(values.length > 0 ? values.join(', ') : 'no permission')
}
return (
<form onSubmit={submit} className="flex flex-col items-start gap-4">
<fieldset className="flex flex-col gap-3">
<legend className="mb-3 text-sm font-medium text-foreground">Permissions of the API key</legend>
{PERMISSIONS.map((permission) => (
<Label key={permission.value} className="font-normal">
<Checkbox name="permissions" value={permission.value} defaultChecked={permission.value === 'read'} />
{permission.label}
</Label>
))}
</fieldset>
<Button type="submit">Create key</Button>
<p role="status" className="text-[13px] text-foreground-light">
{sent !== undefined && `The form sent: ${sent}`}
</p>
</form>
)
}aria-invalid shows the error border. Show the error message as text near the checkbox.
import * as React from 'react'
import { Button, Checkbox, Label, toast } from 'ferry-ui'
export default function CheckboxInvalid() {
const [accepted, setAccepted] = React.useState(false)
const [error, setError] = React.useState(false)
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setError(!accepted)
if (accepted) toast.success('Workspace created')
}
return (
<form noValidate onSubmit={submit} className="flex flex-col items-start gap-3">
<Label>
<Checkbox
checked={accepted}
onCheckedChange={(checked) => {
setAccepted(checked === true)
setError(false)
}}
aria-invalid={error ? true : undefined}
aria-describedby={error ? 'terms-error' : undefined}
/>
I accept the terms of service
</Label>
{error && (
<p id="terms-error" role="alert" className="text-[13px] text-destructive">
Accept the terms to continue.
</p>
)}
<Button type="submit" variant="primary">
Create workspace
</Button>
</form>
)
}aria-label, for example in a table cell.<fieldset> with a <legend>.Checkbox also accepts the props of the Radix UI Checkbox primitive and each attribute of the <button> element.
| Prop | Type | Default |
|---|---|---|
checked | "indeterminate" | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
defaultChecked | "indeterminate" | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
required | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onCheckedChange | ((checked: CheckedState) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||