Checkbox

A checkbox turns one option on or off, and a checkbox group lets people pick any number of options from a list.

A checkbox marks an option as chosen or not chosen, and the choice takes effect when the form is saved. A setting that applies the moment it changes is a Switch. Picking exactly one option from a list is a Radio Group.

Playground

Email me about

Usage

import { CheckboxGroup, Flex, Text } from '@kushagradhawan/kookie-ui';
 
export function NotificationForm() {
  return (
    <Flex direction="column" gap="3">
      <Text as="div" weight="medium" id="notify-label">
        Email me about
      </Text>
      <CheckboxGroup.Root
        name="notify"
        defaultValue={['mentions']}
        aria-labelledby="notify-label"
      >
        <CheckboxGroup.Item value="mentions">Mentions</CheckboxGroup.Item>
        <CheckboxGroup.Item value="comments">Comments on my posts</CheckboxGroup.Item>
        <CheckboxGroup.Item value="digest">Weekly digest</CheckboxGroup.Item>
      </CheckboxGroup.Root>
    </Flex>
  );
}

Anatomy

A single Checkbox stands on its own. A group is a compound component:

<CheckboxGroup.Root>
  <CheckboxGroup.Item value="a">Label</CheckboxGroup.Item>
</CheckboxGroup.Root>
  • CheckboxGroup.Root: Holds the list of checked values and sets size, variant and color for every item.
  • CheckboxGroup.Item: One checkbox. With children it renders a label around the box and the text. Without children it renders the box alone, for your own layout.

Best practices

Use a checkbox when the choice waits for a save. People expect to tick boxes and then press a button. If the change should apply at once, use a Switch.

Write the label as the thing that becomes true. "Send me the weekly digest" reads as a statement people agree to. A negative label such as "Don't send emails" makes a ticked box mean no.

Put every checkbox inside its label. A click on the text then toggles the box, which gives a much larger target than the box alone. CheckboxGroup.Item does this for you, and a single Checkbox goes inside <Text as="label">.

List the options in a column. One option per line is faster to scan than a row, and the group already stacks its items with a gap.

Use indeterminate only for a parent of other checkboxes. It means some children are checked and some are not. It is not a third answer such as "maybe".

Size

size picks the box: 16, 20, 24 and 26 pixels for sizes 1 to 4. Size 2 is the default. The box takes the line height of the text around it, so it lines up with the first line of its label. In a group, the label text follows the group's size.

Examples

Single checkbox

Wrap a lone checkbox and its text in Text as="label", so the text toggles the box too.

<Text as="label" size="2">
  <Flex gap="2">
    <Checkbox defaultChecked />
    Remember me on this device
  </Flex>
</Text>

Select all

A parent checkbox is 'indeterminate' while only some of its children are checked. Clicking it checks them all.

const allChecked = selected.length === rows.length;
const someChecked = selected.length > 0 && !allChecked;
 
<Checkbox
  aria-label="Select all rows"
  checked={allChecked ? true : someChecked ? 'indeterminate' : false}
  onCheckedChange={(checked) => setSelected(checked === true ? rows.map((r) => r.id) : [])}
/>

Required agreement

required stops the form from submitting until the box is checked. Inside a form, the checkbox submits its name and value like a native input.

<Text as="label" size="2">
  <Flex gap="2">
    <Checkbox name="terms" required />
    I agree to the terms of service
  </Flex>
</Text>

Props

Checkbox

PropTypeDefaultDescription
checkedboolean | 'indeterminate'The checked state, controlled
defaultCheckedboolean | 'indeterminate'falseThe checked state at first render
onCheckedChange(checked: boolean | 'indeterminate') => voidFires with the new state
size'1' | '2' | '3' | '4', responsive'2'The box size
variant'raised' | 'solid' | 'soft' | 'outline' | 'surface''raised'The look
colorAccentColorTheme accentThe colour when checked
disabledbooleanfalseBlocks changes
requiredbooleanfalseMust be checked before the form submits
namestringThe field name in a form
valuestring'on'The value sent with the form when checked

Checkbox also accepts every attribute of the HTML button element.

CheckboxGroup.Root

PropTypeDefaultDescription
valuestring[]The checked values, controlled
defaultValuestring[][]The checked values at first render
onValueChange(value: string[]) => voidFires with the new list
size'1' | '2' | '3' | '4', responsive'2'The box and label size
variant'raised' | 'solid' | 'soft' | 'outline' | 'surface''raised'The look of every item
colorAccentColorTheme accentThe colour of every item
namestringThe field name each item submits under
disabledbooleanfalseDisables every item
requiredbooleanfalseAt least one item must be checked before the form submits
dir'ltr' | 'rtl'The reading direction
asChildbooleanfalseRenders the child element as the group

CheckboxGroup.Root also accepts every attribute of the HTML div element.

CheckboxGroup.Item

PropTypeDefaultDescription
valuestringRequired. The value this item adds to the list
disabledbooleanfalseDisables this item

CheckboxGroup.Item also accepts every attribute of the HTML button element.

Accessibility

KeyAction
SpaceChecks or unchecks the focused box
TabMoves focus to the next box, or out of the control
  • Name: The label text names the checkbox when the box sits inside a label. A checkbox with no visible text needs aria-label.
  • State: The box is a button with role="checkbox". A screen reader announces checked, not checked, or mixed for 'indeterminate'.
  • Group: The root has role="group". Give it a name with aria-labelledby that points at the question above the options.
  • Focus: Each box in a group is its own tab stop, as with native checkboxes.