macbetter

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.

Sync these folders
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 checkbox role and aria-checked (mixed for indeterminate), plus a hidden native checkbox for forms.
  • Space toggles it. Its title, or aria-label without 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

PropTypeDefaultDescription
childrenReactNode—The title. With it, the box and the text form one label.
descriptionReactNode—Secondary text under the title.
checkedboolean | "indeterminate"—The state. Pass it to control the checkbox.
defaultCheckedboolean | "indeterminate"falseThe state at first, when uncontrolled.
onCheckedChange(checked, details) => void—Runs when it wants to change.
valuestring—What it submits, and its value inside a group.
parentbooleanfalseChecks or unchecks every checkbox in its group.
disabledbooleanfalseCan't be focused or changed.
readOnlybooleanfalseFocusable and submitted, but doesn't change.
requiredbooleanfalseBlocks form submission until it's checked.
namestring—The name it submits under.

CheckboxGroup

PropTypeDefaultDescription
valuestring[]—The checked values. Pass it to control the group.
defaultValuestring[]—The values checked at first.
onValueChange(value, details) => void—Runs when the checked values want to change.
allValuesstring[]—Every value a parent checkbox checks.
disabledbooleanfalseDisables every checkbox.
namestring—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

AttributeOnValues
data-checked / data-uncheckedCheckbox, indicatorThe state
data-indeterminateCheckbox, indicatorPresent while mixed
data-disabled, data-readonly, data-requiredCheckboxPresent in those states
data-starting-style, data-ending-styleIndicatorPresent while the mark animates in or out

CSS variables

VariablemacOSiOSWhat it sets
--mb-checkbox-size14px22pxThe box
--mb-checkbox-radius3.5pxFullIts corners
--mb-checkbox-stroke2.11.7Checkmark weight, in a 16-unit box
--mb-checkbox-label-gap6px12pxBetween the box and its title
--mb-checkbox-group-gap6px16pxBetween checkboxes in a group