Buttons and controls
Checkbox
Turn an option on or off, or choose several from a list.
import { Checkbox } from "macbetter/checkbox"
export default function Example() {
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--mb-checkbox-group-gap)" }}>
<Checkbox defaultChecked>Show hidden files</Checkbox>
<Checkbox>Show all filename extensions</Checkbox>
<Checkbox defaultChecked="indeterminate">Keep folders on top</Checkbox>
</div>
)
}When to use it
Use a checkbox for an option that's either on or off, especially in a window or form whose changes apply together, and for choosing any number of items from a list. Use a Switch for a setting that takes effect the moment it changes, and Radio buttons when only one choice is allowed.
- Write the title so that checked means yes: "Show hidden files", not "Hide hidden files".
- Use the mixed state only for a parent that summarizes a group, like "Select all".
- On iOS and iPadOS, checkboxes appear as selection circles, as in Reminders and Mail. Most iOS settings are switches instead.
Usage
import { Checkbox, CheckboxGroup } from "macbetter/checkbox"Pass the title as children and the checkbox and its text become one label: clicking the text toggles the box. Without children it renders the box alone, for a table cell or a Field.
Description
description adds a line of secondary text under the title. It's part of the label.
import { Checkbox } from "macbetter/checkbox"
export default function Example() {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 14, maxWidth: 360 }}>
<Checkbox defaultChecked description="Downloads apps you buy on your other devices.">
Automatic downloads
</Checkbox>
<Checkbox description="Installs updates overnight while your Mac is charging.">
Install app updates
</Checkbox>
</div>
)
}Group
CheckboxGroup keeps one list of checked values. Give each checkbox a value and name the group.
import { Checkbox, CheckboxGroup } from "macbetter/checkbox"
const folders = [
{ value: "desktop", label: "Desktop" },
{ value: "documents", label: "Documents" },
{ value: "downloads", label: "Downloads" },
{ value: "pictures", label: "Pictures" },
]
export default function Example() {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
<span id="sync-folders" style={{ font: "var(--mb-text-headline)" }}>
Sync these folders
</span>
<CheckboxGroup aria-labelledby="sync-folders" defaultValue={["desktop", "documents"]}>
{folders.map((folder) => (
<Checkbox key={folder.value} value={folder.value}>
{folder.label}
</Checkbox>
))}
</CheckboxGroup>
</div>
)
}Select all
A checkbox with parent checks or unchecks the whole group, and shows the mixed state when only some are checked. Pass allValues so it renders right on the server.
import { Checkbox, CheckboxGroup } from "macbetter/checkbox"
const notifications = [
{ value: "messages", label: "Messages" },
{ value: "mentions", label: "Mentions" },
{ value: "reactions", label: "Reactions" },
]
export default function Example() {
return (
<CheckboxGroup
aria-label="Notifications"
defaultValue={["messages", "mentions"]}
allValues={notifications.map((item) => item.value)}
>
<Checkbox parent>All notifications</Checkbox>
<div style={{
display: "flex",
flexDirection: "column",
gap: "var(--mb-checkbox-group-gap)",
paddingInlineStart: "calc(var(--mb-checkbox-size) + var(--mb-checkbox-label-gap))",
}}>
{notifications.map((item) => (
<Checkbox key={item.value} value={item.value}>
{item.label}
</Checkbox>
))}
</div>
</CheckboxGroup>
)
}Disabled
import { Checkbox } from "macbetter/checkbox"
export default function Example() {
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--mb-checkbox-group-gap)" }}>
<Checkbox disabled defaultChecked>
Use iCloud Keychain
</Checkbox>
<Checkbox disabled>Share across devices</Checkbox>
</div>
)
}Controlled
Pass checked and onCheckedChange. The value is true, false or "indeterminate".
"use client"
import { Button } from "macbetter/button"
import { Checkbox } from "macbetter/checkbox"
import { useState } from "react"
export default function Example() {
const [agreed, setAgreed] = useState(false)
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 14 }}>
<Checkbox checked={agreed} onCheckedChange={(checked) => setAgreed(checked === true)}>
I agree to the Terms of Service
</Checkbox>
<Button variant="filled" disabled={!agreed}>
Create Account
</Button>
</div>
)
}Platform behavior
- macOS. A 14-point rounded square with a hairline edge. Checked, it fills with the accent and a checkmark draws itself in, stroke first. Mixed shows a dash.
- iPadOS and iOS. iOS has no square checkbox, so macbetter draws the 22-point selection circle: an outlined ring that fills with the accent and a checkmark, with a small pop. Titles use 17-point body text and the rows space out for touch.
- Every platform. The checkmark uses the theme's on-accent color, so it reads on any accent. The hit area extends to the platform minimum.
Accessibility
- Renders a button with the
checkboxrole andaria-checked(mixedfor indeterminate), plus a hidden native checkbox for forms. - Space toggles it. Its title, or
aria-labelwithout one, names it; the description is part of the label. - A parent checkbox points at the boxes it controls with
aria-controls. - Checked is shown by a fill and a mark, not color alone.
API
Checkbox
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The title. With it, the box and the text form one label. |
description | ReactNode | — | Secondary text under the title. |
checked | boolean | "indeterminate" | — | The state. Pass it to control the checkbox. |
defaultChecked | boolean | "indeterminate" | false | The state at first, when uncontrolled. |
onCheckedChange | (checked, details) => void | — | Runs when it wants to change. |
value | string | — | What it submits, and its value inside a group. |
parent | boolean | false | Checks or unchecks every checkbox in its group. |
disabled | boolean | false | Can't be focused or changed. |
readOnly | boolean | false | Focusable and submitted, but doesn't change. |
required | boolean | false | Blocks form submission until it's checked. |
name | string | — | The name it submits under. |
CheckboxGroup
| Prop | Type | Default | Description |
|---|---|---|---|
value | string[] | — | The checked values. Pass it to control the group. |
defaultValue | string[] | — | The values checked at first. |
onValueChange | (value, details) => void | — | Runs when the checked values want to change. |
allValues | string[] | — | Every value a parent checkbox checks. |
disabled | boolean | false | Disables every checkbox. |
name | string | — | The name every checkbox submits under. |
CheckboxIndicator is exported too, for a checkbox you compose yourself; it draws the checkmark and the dash.
Data attributes
| Attribute | On | Values |
|---|---|---|
data-checked / data-unchecked | Checkbox, indicator | The state |
data-indeterminate | Checkbox, indicator | Present while mixed |
data-disabled, data-readonly, data-required | Checkbox | Present in those states |
data-starting-style, data-ending-style | Indicator | Present while the mark animates in or out |
CSS variables
| Variable | macOS | iOS | What it sets |
|---|---|---|---|
--mb-checkbox-size | 14px | 22px | The box |
--mb-checkbox-radius | 3.5px | Full | Its corners |
--mb-checkbox-stroke | 2.1 | 1.7 | Checkmark weight, in a 16-unit box |
--mb-checkbox-label-gap | 6px | 12px | Between the box and its title |
--mb-checkbox-group-gap | 6px | 16px | Between checkboxes in a group |