Checkbox

User Interface (UI)

How a checkbox looks, from its parts and sizes to its states and tokens.

Let's start with how a checkbox looks: its parts, its sizes and how it shows each of its states.

Anatomy

  1. Container — The <label>, which wraps the box and its label, so a press on either toggles it. It carries the size.
  2. Box — The <input type="checkbox"> itself, which holds the state and takes focus.
  3. Indicator — A check, or a dash while it's indeterminate, drawn over the box and hidden from assistive technologies.
  4. Label — Optional. The text that says what checking the box means, which names it.
  5. Hit area — At least 24 × 24px around the box, which a press anywhere in toggles it.
  6. Focus ring — An outline around the box, drawn outside it so nothing moves.

Every checkbox has a container, a box, an indicator and a hit area. The label is optional only for a checkbox that its context already names, like one in each row of a table, which takes an aria-label instead (see Label every checkbox). Any hint and error message sit outside the container, in the page, and point to the box by its id. The parts always render in the same order, which you can see in the DOM contract.

Sizes

The size prop scales the box, the text and the gap between them together, and md is the default. The names are the same as the Button's and the Input's, and so is the text, so a checkbox reads at the same size as the inputs and buttons next to it.

<Checkbox size="xs">Email me updates</Checkbox>
{/* …sm, md, lg, xl */}
SizeBoxTextGapRadiusHit area
xs14px12px6px4px24px
sm16px14px8px4px24px
md18px14px8px4px24px
lg20px16px8px6px24px
xl24px16px10px6px24px

Sizes are set in rem, and the table shows the values we recommend at the default text size of 16px. The box grows with the text, a little larger than its letters, so it lines up with the first line of its label at every size. Whatever its size, the box sits in a hit area of at least 24 × 24px (WCAG 2.5.8), which is part of the <label>, so a press anywhere in it toggles the box. Each checkbox is at least as tall as its hit area, so two stacked checkboxes never share one.

States

Each state has a look of its own, styled from the <input> itself. That way, what people see and what assistive technologies report can never drift apart, and a box can't look checked without actually being checked.

Unchecked (try hovering, clicking or tabbing to it)
Checked
Indeterminate
Disabled
Read-only (try clicking or pressing Space)

Agree to the terms to create an account

Invalid
<Checkbox>Email me updates</Checkbox>
<Checkbox defaultChecked>Email me updates</Checkbox>
<Checkbox indeterminate>Select all</Checkbox>

<Checkbox disabled>Text me updates</Checkbox>
<Checkbox defaultChecked disabled>Email me updates</Checkbox>

<Checkbox readOnly>Text me updates</Checkbox>
<Checkbox defaultChecked readOnly>Email me updates</Checkbox>

<Checkbox required aria-invalid="true" aria-describedby="terms-error">I agree to the terms</Checkbox>
<p id="terms-error">Agree to the terms to create an account</p>
StateSelectorRecommended look
Hover:hover, inside @media (hover: hover)The border at 80% of the text colour, up from 55%
Focus> input:focus-visibleA 2px outline, 2px from the box, in --color--focus
Checked> input:checkedThe box filled with --color--primary, with a check on top
Indeterminate> input:indeterminateThe same fill, with a dash in place of the check
Disabled:has(> input:disabled)50% opacity and a not-allowed cursor, label included
Read-only> input[aria-readonly="true"]A dashed border, with the check in the text colour, no fill
Invalid> input[aria-invalid="true"]A 2px border in --color--error-text

The checked and indeterminate looks share their fill, and tell themselves apart by their mark, a check or a dash, rather than by colour (WCAG 1.4.1). The read-only look is dashed, like a read-only Input's, and drops the fill, so a checked box still shows its check without looking like one you can change. The disabled look comes from :disabled, which also matches every checkbox inside a <fieldset disabled>, so a whole group looks the way it behaves. We recommend leaving :invalid out of it, as on the Input, since it matches an unchecked required box before anyone has touched it. The invalid look comes from aria-invalid instead, which you set when the error message shows.

None of the states change the checkbox's size or position. The invalid border's second pixel is drawn inside the box, and the focus ring outside it. You'll find how each state behaves under Every state.

Tokens

The checkbox reads five theme tokens, which every component shares:

TokenUsed for
--color--neutral-textThe label, and mixed into the box's border
--color--primaryThe fill of a checked or indeterminate box
--color--primary-contrastThe check and the dash, on top of the fill
--color--error-textThe border of an invalid box
--color--focusThe focus ring, which is the same for every component

The border is the text colour at 55%, and 80% on hover. An unchecked box is nothing but its border, so the border needs 3:1 against the page, and so does the fill of a checked box, while the check needs 3:1 against the fill (WCAG 1.4.11). With our reference tokens, from the Tailwind CSS palette, the border comes to 3.5:1, the fill to 6.4:1 and the check to 6.4:1 on white, and 5.0:1, 5.7:1 and 6.4:1 on zinc-900. If you change the tokens, make sure to check the same pairs. You'll find the tokens themselves in the Button's tokens.css, which you can copy into your theme.

If you need to tune a checkbox, you can set its own variables, such as --checkbox--box--radius, --checkbox--box--fill or --checkbox--indicator--color, from a class. The Checkbox's styles sit in the components cascade layer, so your class wins without any specificity tricks:

Terms.tsx
import styles from "./Terms.module.css";

export function Terms() {
  return <Checkbox name="terms" className={styles.square}>I agree to the terms</Checkbox>;
}
Terms.module.css
.square {
  --checkbox--box--radius: 0;
}

UI Checklist

RuleLevelScopeRequirementBasisCheck
checkbox/contrastMustComponentThe label has a contrast of 4.5:1, and the focus ring has 3:1 against the pageWCAG 1.4.3, 1.4.11 (AA)axe color-contrast, contrast checker
checkbox/boundary-contrastMustComponentThe box's border and a checked box's fill have 3:1 against the page, and the check and the dash have 3:1 against the fillWCAG 1.4.11 (AA)Contrast checker
checkbox/focus-visibleMustComponentKeyboard focus shows an outline at least 2px thick around the box, outside its border, on :focus-visibleWCAG 2.4.7 (AA), 2.4.13 (AAA)Keyboard
checkbox/target-sizeMustComponentThe box has a hit area of at least 24 × 24px that toggles it, without counting its labelOpen Components, beyond WCAG 2.5.8 (AA)axe target-size, emulation
checkbox/distinct-marksMustComponentChecked shows a check and indeterminate a dash, so the two never rely on colour to tell them apartWCAG 1.4.1 (A)Visual regression test
checkbox/resizableMustComponentThe box, its gap and its hit area are sized in rem, and the label wraps beside the box instead of being clippedOpen Components, beyond WCAG 1.4.4, 1.4.12 (AA)200% zoom
checkbox/stable-sizeMustComponentNo state changes the checkbox's size or positionOpen ComponentsVisual regression test
checkbox/state-selectorsShouldComponentStates are styled from the <input>, with :checked, :indeterminate, [aria-readonly], :disabled and [aria-invalid], and never from :invalid or :read-onlyOpen ComponentsReview
checkbox/hover-capableShouldComponentHover styles only apply inside @media (hover: hover)Open ComponentsReview
checkbox/forced-colorsMustComponentThe box, its check or dash, and its focus, read-only and disabled states stay visible in forced colours modeOpen ComponentsEmulation
checkbox/more-contrastShouldComponentThe box's border is drawn at full strength when prefers-contrast: more is setOpen ComponentsEmulation

FAQ

Copyright © 2026