Checkbox

Checkbox gives users a binary choice between multiple options in a series.

Give FeedbackWAI-ARIABundle Size
'use client';
import * as React from 'react';
import { Checkbox } from '@base_ui/react/Checkbox';
import { useTheme } from '@mui/system';

export default function UnstyledCheckboxIntroduction() {
  return (
    <div style={{ display: 'flex', gap: 12 }}>
      <Checkbox.Root
        className="Checkbox"
        aria-label="Basic checkbox, on by default"
        defaultChecked
      >
        <Checkbox.Indicator className="Checkbox-indicator">
          <CheckIcon className="Checkbox-icon" />
        </Checkbox.Indicator>
      </Checkbox.Root>
      <Checkbox.Root
        className="Checkbox"
        aria-label="Basic checkbox, off by default"
      >
        <Checkbox.Indicator className="Checkbox-indicator">
          <CheckIcon className="Checkbox-icon" />
        </Checkbox.Indicator>
      </Checkbox.Root>
      <Checkbox.Root
        className="Checkbox"
        aria-label="Disabled checkbox, on by default"
        defaultChecked
        disabled
      >
        <Checkbox.Indicator className="Checkbox-indicator">
          <CheckIcon className="Checkbox-icon" />
        </Checkbox.Indicator>
      </Checkbox.Root>
      <Checkbox.Root
        className="Checkbox"
        aria-label="Disabled checkbox, off by default"
        disabled
      >
        <Checkbox.Indicator className="Checkbox-indicator">
          <CheckIcon className="Checkbox-icon" />
        </Checkbox.Indicator>
      </Checkbox.Root>
      <Styles />
    </div>
  );
}

const grey = {
  100: '#E5EAF2',
  300: '#C7D0DD',
  500: '#9DA8B7',
  600: '#6B7A90',
  800: '#303740',
  900: '#1C2025',
};

function useIsDarkMode() {
  const theme = useTheme();
  return theme.palette.mode === 'dark';
}

function Styles() {
  // Replace this with your app logic for determining dark mode
  const isDarkMode = useIsDarkMode();

  return (
    <style>
      {`
      .Checkbox {
        all: unset;
        box-sizing: border-box;
        text-align: center;
        width: 24px;
        height: 24px;
        padding: 0;
        border-radius: 4px;
        border: 2px solid ${grey[600]};
        background: none;
        transition-property: background, border-color;
        transition-duration: 0.15s;
      }

      .Checkbox[data-disabled] {
        opacity: 0.4;
        cursor: not-allowed;
      }

      .Checkbox:focus-visible {
        outline: 2px solid ${isDarkMode ? grey[600] : grey[500]};
        outline-offset: 2px;
      }

      .Checkbox[data-checked] {
        border-color: ${grey[800]};
        background: ${grey[800]};
      }

      .Checkbox-indicator {
        height: 100%;
        display: inline-block;
        visibility: hidden;
        color: ${isDarkMode ? grey[900] : grey[100]};
      }

      .Checkbox-indicator[data-checked] {
        visibility: visible;
      }

      .Checkbox-icon {
        width: 100%;
        height: 100%;
      }

      @media (prefers-color-scheme: dark) {
        .Checkbox {
          border-color: ${grey[500]};
        }

        .Checkbox[data-checked] {
          border-color: ${grey[300]};
          background: ${grey[300]};
        }

        .Checkbox:hover:not([data-disabled]) {
          border-color: ${grey[100]};
        }

        .Checkbox-indicator {
          color: ${grey[900]};
        }
      }
    `}
    </style>
  );
}

function CheckIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      {...props}
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
    >
      <path
        d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"
        fill="currentColor"
      />
    </svg>
  );
}

Installation

Base UI components are all available as a single package.

npm install @base_ui/react

Once you have the package installed, import the component.

import { Checkbox } from '@base_ui/react/Checkbox';

Anatomy

Checkbox is composed of two components:

<Checkbox.Root>
  <Checkbox.Indicator />
</Checkbox.Root>

Indeterminate state

To make the Checkbox indeterminate, add the indeterminate prop to override the appearance of the Checkbox. The Checkbox remains in an indeterminate state regardless of user interaction until set back to false.

'use client';
import * as React from 'react';
import { styled } from '@mui/system';
import { Checkbox as BaseCheckbox } from '@base_ui/react/Checkbox';

export default function UnstyledCheckboxIndeterminate() {
  return (
    <div style={{ display: 'flex', gap: 12 }}>
      <Checkbox aria-label="Indeterminate checkbox" indeterminate>
        <Indicator>
          <HorizontalRuleIcon />
        </Indicator>
      </Checkbox>
      <Checkbox aria-label="Indeterminate disabled checkbox" indeterminate disabled>
        <Indicator>
          <HorizontalRuleIcon />
        </Indicator>
      </Checkbox>
    </div>
  );
}

const blue = {
  400: '#3399FF',
  600: '#0072E6',
  800: '#004C99',
};

const grey = {
  100: '#E5EAF2',
};

const Checkbox = styled(BaseCheckbox.Root)(
  ({ theme }) => `
    width: 24px;
    height: 24px;
    padding: 0;
    border-radius: 4px;
    border: 2px solid ${blue[600]};
    background: none;
    transition-property: background, border-color;
    transition-duration: 0.15s;
    outline: none;

    &[data-disabled] {
      opacity: 0.4;
      cursor: not-allowed;
    }

    &:focus-visible {
      outline: 2px solid ${theme.palette.mode === 'dark' ? blue[800] : blue[400]};
      outline-offset: 2px;
    }

    &[data-checked],
    &[data-indeterminate] {
      border-color: transparent;
      background: ${blue[600]};
    }
  `,
);

const HorizontalRuleIcon = styled(function HorizontalRuleIcon(
  props: React.SVGProps<SVGSVGElement>,
) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      {...props}
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
    >
      <path d="M4 11h16v2H4z" fill="currentColor" />
    </svg>
  );
})`
  height: 100%;
  width: 100%;
`;

const Indicator = styled(BaseCheckbox.Indicator)`
  color: ${grey[100]};
  height: 100%;
  display: inline-block;
  visibility: hidden;

  &[data-checked],
  &[data-indeterminate] {
    visibility: visible;
  }
`;

The primary use case for an indeterminate checkbox is representing the state of a parent checkbox where only some of its children are checked.

'use client';
import * as React from 'react';
import { styled } from '@mui/system';
import { Checkbox as BaseCheckbox } from '@base_ui/react/Checkbox';

const colors = ['Red', 'Green', 'Blue'];

export default function UnstyledCheckboxIndeterminateGroup() {
  const [checkedValues, setCheckedValues] = React.useState(['Green']);

  const isChecked = checkedValues.length === colors.length;
  const isIndeterminate =
    checkedValues.length !== colors.length && checkedValues.length > 0;

  const id = React.useId();

  return (
    <div style={{ display: 'flex', flexDirection: 'column' }}>
      <ListRoot>
        <Checkbox
          id={id}
          aria-controls={colors.map((color) => `${id}-${color}`).join(' ')}
          indeterminate={isIndeterminate}
          checked={isChecked}
          onCheckedChange={(checked) => {
            setCheckedValues(checked ? colors : []);
          }}
        >
          <Indicator>
            {isIndeterminate ? <HorizontalRuleIcon /> : <CheckIcon />}
          </Indicator>
        </Checkbox>
        <Label htmlFor={id} onMouseDown={(event) => event.preventDefault()}>
          Colors
        </Label>
      </ListRoot>
      <List>
        {colors.map((color) => (
          <ListItem key={color}>
            <Checkbox
              id={`${id}-${color}`}
              checked={checkedValues.includes(color)}
              onCheckedChange={(checked) => {
                const newCheckedValues = [...checkedValues];
                if (checked) {
                  newCheckedValues.push(color);
                } else {
                  newCheckedValues.splice(newCheckedValues.indexOf(color), 1);
                }
                setCheckedValues(newCheckedValues);
              }}
            >
              <Indicator>
                <CheckIcon />
              </Indicator>
            </Checkbox>
            <Label
              htmlFor={`${id}-${color}`}
              onMouseDown={(event) => event.preventDefault()}
            >
              {color}
            </Label>
          </ListItem>
        ))}
      </List>
    </div>
  );
}

const blue = {
  400: '#3399FF',
  600: '#0072E6',
  800: '#004C99',
};

const grey = {
  100: '#E5EAF2',
  400: '#B0B8C4',
  800: '#303740',
};

const Checkbox = styled(BaseCheckbox.Root)(
  ({ theme }) => `
    width: 24px;
    height: 24px;
    padding: 0;
    border-radius: 4px;
    border: 2px solid ${blue[600]};
    background: none;
    transition-property: background, border-color;
    transition-duration: 0.15s;
    outline: none;

    &[data-disabled] {
      opacity: 0.4;
      cursor: not-allowed;
    }

    &:focus-visible {
      outline: 2px solid ${theme.palette.mode === 'dark' ? blue[800] : blue[400]};
      outline-offset: 2px;
    }

    &[data-checked], &[data-indeterminate] {
      border-color: transparent;
      background: ${blue[600]};
    }
  `,
);

const HorizontalRuleIcon = styled(function HorizontalRuleIcon(
  props: React.SVGProps<SVGSVGElement>,
) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      {...props}
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
    >
      <path d="M4 11h16v2H4z" fill="currentColor" />
    </svg>
  );
})`
  height: 100%;
  width: 100%;
`;

const CheckIcon = styled(function CheckIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      {...props}
      width="24"
      height="24"
      viewBox="0 0 24 24"
      fill="none"
    >
      <path
        d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"
        fill="currentColor"
      />
    </svg>
  );
})`
  height: 100%;
  width: 100%;
`;

const Indicator = styled(BaseCheckbox.Indicator)`
  height: 100%;
  display: inline-block;
  visibility: hidden;
  color: ${grey[100]};

  &[data-checked],
  &[data-indeterminate] {
    visibility: visible;
  }
`;

const ListRoot = styled('div')`
  display: flex;
  align-items: center;
  margin-bottom: 8px;
`;

const List = styled('ul')`
  list-style: none;
  padding: 0;
  margin: 0;
  margin-left: 32px;
`;

const ListItem = styled('li')`
  display: flex;
  align-items: center;

  &:not(:last-child) {
    margin-bottom: 8px;
  }
`;

const Label = styled('label')(
  ({ theme }) => `
    padding-left: 8px;
    color: ${theme.palette.mode === 'dark' ? grey[400] : grey[800]};
  `,
);

It's a visual-only state, so its internal checked state can still be changed.

Overriding default components

Use the render prop to override the rendered checkbox or indicator element with your own components.

<Checkbox.Root render={(props) => <MyCheckbox {...props} />}>
  <Checkbox.Indicator render={(props) => <MyCheckboxIndicator {...props} />} />
</Checkbox.Root>

Accessibility

Ensure the Checkbox has an accessible name via a <label> element.

<Checkbox.Root id="my-checkbox">
  <Checkbox.Indicator />
</Checkbox.Root>
<label htmlFor="my-checkbox">
  My label
</label>

API Reference

CheckboxRoot

The foundation for building custom-styled checkboxes.

PropTypeDefaultDescription
checkedboolundefinedIf true, the component is checked.
classNameunionClass names applied to the element or a function that returns them based on the component's state.
defaultCheckedboolfalseThe default checked state. Use when the component is not controlled.
disabledboolfalseIf true, the component is disabled.
indeterminateboolfalseIf true, the checkbox will be indeterminate.
inputRefunionThe ref to the input element.
namestringundefinedName of the underlying input element.
onCheckedChangefuncCallback fired when the checked state is changed.
parentboolfalseIf true, the checkbox is a parent checkbox for a group of child checkboxes.
readOnlyboolfalseIf true, the component is read only.
renderunionA function to customize rendering of the component.
requiredboolfalseIf true, the input element is required.

CheckboxIndicator

The indicator part of the Checkbox.

PropTypeDefaultDescription
classNameunionClass names applied to the element or a function that returns them based on the component's state.
keepMountedbooltrueIf true, the indicator stays mounted when unchecked. Useful for CSS animations.
renderunionA function to customize rendering of the component.

Contents