Skip to content

Checkbox

Allows users to select one or more options from a set of choices.

  • Check
  • Tick Box

Overview

The Checkbox is a form control that allows users to select one or more options from a set — or to toggle a single binary choice. It's one of the most fundamental input elements in web interfaces, appearing in forms, settings pages, filter panels, Table row selection, and consent flows.

Unlike a Radio Button (which enforces a single selection from a group), checkboxes are independent — each checkbox operates on its own, and selecting one doesn't deselect another. This independence makes checkboxes the right choice when users need to pick zero, one, or many options from a list.

The checkbox also supports a unique third state: indeterminate. This "partially checked" state appears when a parent checkbox represents a group where only some children are selected — common in tree views, bulk selection interfaces, and "select all" patterns.

When to use a Checkbox:

  • For binary yes/no choices ("Agree to terms", "Remember me", "Subscribe to newsletter")
  • For multi-selection from a list of non-exclusive options
  • For "select all" with indeterminate state in Tables or Lists
  • For settings and preferences with on/off states where the change isn't instant

When NOT to use a Checkbox:

  • For mutually exclusive choices — use a Radio Button group
  • For an immediate on/off toggle — use a Switch (checkboxes imply a "save" step; switches apply instantly)
  • For selecting from a long list (10+ options) — use a Select with multi-select or a Combobox with checkboxes
  • For a single, labeled action — consider a Button instead

The Switch vs. Checkbox decision trips up many designers: if the change takes effect immediately (dark mode toggle, notification mute), use a Switch. If the change is deferred until a form is submitted, use a Checkbox.

Validate your check and label colors with the Contrast Checker to ensure the checked state is clearly visible.

Variants

Visual Variants

VariantDescriptionUse Case
StandardSquare box with a checkmark when selectedDefault for all form contexts
IndeterminateSquare box with a horizontal dash (–)Parent checkbox when some children are selected
Card checkboxEntire card surface acts as the checkbox areaFeature selection, plan comparison, settings groups
Chip checkboxStyled as a selectable Tag or chipFilter interfaces, multi-select tags

Size Variants

SizeBox DimensionLabel Font SizeUse Case
Small16×16px13pxDense forms, table rows, compact UIs
Medium20×20px14pxDefault for most forms
Large24×24px16pxSettings pages, mobile layouts, accessibility-focused UIs

Label Position

PositionDescription
Right (default)Label text to the right of the checkbox. Standard for LTR languages.
LeftLabel text to the left. Common in right-aligned forms or settings panels.
HiddenVisually hidden label (still accessible via aria-label). For checkboxes in table rows where the column header serves as the label.

Properties

Checkbox Properties

PropertyTypeDefaultDescription
checkedbooleanfalseWhether the checkbox is checked (controlled)
defaultCheckedbooleanfalseInitial checked state (uncontrolled)
indeterminatebooleanfalseShows the indeterminate (–) state. Note: this is a DOM property, not an HTML attribute — it must be set via JavaScript.
onChange(checked: boolean) => void—Callback when the checked state changes
disabledbooleanfalsePrevents interaction
requiredbooleanfalseMarks the field as required for form validation
namestring—Form field name for submission
valuestring'on'Value submitted with the form when checked
labelReactNode—Label content. Can be a string or JSX with links (e.g., "I agree to the terms")
descriptionstring—Helper text below the label
errorstring—Error message shown below the checkbox
size'sm' | 'md' | 'lg''md'Checkbox size variant

Important: The indeterminate state is visual only — it does not affect the underlying checked value. When a user clicks an indeterminate checkbox, you decide what happens (typically it becomes fully checked). This logic is your responsibility.

Token Mappings

Design Token Mappings

Token CategoryToken ExampleCheckbox Usage
Color – Unchecked Border--color-border-strongBorder of the unchecked box
Color – Checked Fill--color-primary-600Background when checked
Color – Checkmark--color-on-primary (white)The checkmark/dash icon color
Color – Hover--color-primary-700Checked hover state
Color – Disabled--color-border-disabled, --color-fill-disabledReduced contrast when disabled
Color – Error--color-error-600Border color when in error state
Color – Focus Ring--color-focus-ringFocus indicator color
Border Radius--radius-xs (3px) or --radius-sm (4px)Checkbox box corners (square-ish, not round — that implies radio)
Spacing – Gap--space-2 (8px)Gap between checkbox and label
Spacing – Group--space-3 (12px)Vertical gap between checkboxes in a group
Typography--font-size-sm, --font-weight-normalLabel text
Transition--duration-fast (100ms)Check animation

For a full reference on token architecture, see our Design Tokens Complete Guide.

States

Checkbox States

StateVisual Treatment
UncheckedEmpty box with visible border. Border color: --color-border-strong.
CheckedFilled box with checkmark (✓). Background: --color-primary-600. Checkmark: white.
IndeterminateFilled box with horizontal dash (–). Same colors as checked.
HoverBorder darkens (unchecked) or fill lightens (checked). Subtle background highlight on the label row.
FocusVisible focus ring (2px solid, 2px offset). Must meet 3:1 contrast per WCAG SC 1.4.11.
Active / PressedBrief scale-down (transform: scale(0.95)) on the box for tactile feedback.
DisabledReduced opacity (0.4). No hover/focus effects. cursor: not-allowed.
Disabled + CheckedMuted fill color. Checkmark visible but low-contrast.
ErrorBorder color switches to --color-error-600. Error message appears below.
RequiredSmall asterisk or "required" indicator near the label. Does not change the checkbox itself.

Check Animation

A well-crafted check animation adds polish. The checkmark should "draw" in (stroke-dashoffset animation on an SVG path) over 100–150ms. Avoid bouncy or slow animations — the checkbox should feel instant and crisp.

.checkbox-icon path {
  stroke-dasharray: 20;
  stroke-dashoffset: 20;
  transition: stroke-dashoffset 150ms ease-out;
}
.checkbox[aria-checked="true"] .checkbox-icon path {
  stroke-dashoffset: 0;
}

Accessibility

WCAG Requirements

CriterionLevelRequirement
SC 1.3.1 Info and RelationshipsACheckbox must be programmatically associated with its label via <label for="id"> or wrapping <label>.
SC 1.4.3 Contrast (Minimum)AALabel text: 4.5:1 contrast. Verify with Contrast Checker.
SC 1.4.11 Non-text ContrastAACheckbox border and checkmark: 3:1 contrast against adjacent colors. The unchecked border must be clearly visible against the background.
SC 2.5.8 Target Size (Minimum)AAMinimum 24×24px clickable area (WCAG 2.2). The hit area includes the label — make the <label> clickable.
SC 3.3.2 Labels or InstructionsAEvery checkbox must have a visible label.
SC 4.1.2 Name, Role, ValueAUse native <input type="checkbox"> for automatic role, state, and name. Custom checkboxes must implement role="checkbox" with aria-checked.

ARIA Attributes

  • Native checkbox (<input type="checkbox">): Needs no ARIA — the role and checked state are built in. Just pair it with a <label>.
  • Custom checkbox (<div role="checkbox">): Must implement role="checkbox", aria-checked="true|false|mixed", tabindex="0", and handle Space key to toggle.
  • Indeterminate: Use aria-checked="mixed" for custom implementations. For native checkboxes, set the .indeterminate DOM property (there's no HTML attribute for it).
  • Checkbox group: Wrap in a <fieldset> with a <legend> to group related checkboxes. The legend becomes the group label for screen readers.
  • Error state: Associate error text with aria-describedby and use aria-invalid="true".

Keyboard Interaction

KeyAction
SpaceToggles the checkbox checked state
TabMoves focus to the next focusable element
Shift + TabMoves focus to the previous focusable element

Note: Unlike radio buttons, checkboxes in a group do not use arrow keys for navigation. Each checkbox is independently focusable via Tab. This is an important distinction.

Common Mistakes

  1. Forgetting to make the label clickable. The label should toggle the checkbox — not just sit next to it. Use <label> wrapping or for attribute.
  2. Using aria-checked on a native checkbox. Don't — it's redundant and can cause double announcements.
  3. Not handling indeterminate in screen readers. Native <input> with .indeterminate = true announces as "mixed" in most screen readers. Verify this.

For more, see our ARIA Attributes Guide and Keyboard Accessibility Guide.

Usage Guidelines

Do's

  • ✅ Always provide a visible label. Even in table rows, ensure column headers serve as the implicit label, and add aria-label for screen readers.
  • ✅ Use a fieldset + legend for checkbox groups. This gives screen readers the group context ("Notification preferences: Email, SMS, Push").
  • ✅ Make the entire row clickable (label + checkbox), not just the tiny box. This is a massive usability win on touch devices.
  • ✅ List options in a logical order — alphabetical, by frequency, or by importance. Don't randomize.
  • ✅ Use positive framing. "Enable notifications" not "Disable notification silence." Double negatives confuse everyone.
  • ✅ Show selected count when checkboxes control a bulk action: "3 items selected".

Don'ts

  • ❌ Don't use a checkbox for a single binary choice that takes effect immediately. That's a Switch.
  • ❌ Don't pre-check opt-in checkboxes. Especially for marketing consent — this may violate GDPR and is hostile UX. Default to unchecked.
  • ❌ Don't use more than 7 checkboxes in a visible group without structure. Beyond that, add subgroups, search, or a different pattern entirely.
  • ❌ Don't align checkboxes horizontally unless you have 2–3 short-label options. Vertical stacking is far more scannable.
  • ❌ Don't use round checkboxes. Square = multi-select (checkbox). Round = single-select (radio). This convention is deeply ingrained. Breaking it confuses users.

Content Guidelines

  • Labels: Sentence case, positive framing, 1–5 words
  • Group legend: Descriptive question or statement ("Which notifications would you like?")
  • Required groups: Add "(required)" to the legend or use aria-required on the group
  • Error messages: Be specific ("Select at least one notification method" not "Required field")

Code Snippets

html
<!-- Single Checkbox -->
<div class="checkbox-field">
  <input type="checkbox" id="terms" name="terms" required />
  <label for="terms">
    I agree to the <a href="/terms">terms of service</a>
  </label>
</div>

<!-- Checkbox Group -->
<fieldset class="checkbox-group">
  <legend>Notification preferences</legend>

  <div class="checkbox-field">
    <input type="checkbox" id="notif-email" name="notifications" value="email" checked />
    <label for="notif-email">Email</label>
  </div>

  <div class="checkbox-field">
    <input type="checkbox" id="notif-sms" name="notifications" value="sms" />
    <label for="notif-sms">SMS</label>
  </div>

  <div class="checkbox-field">
    <input type="checkbox" id="notif-push" name="notifications" value="push" checked />
    <label for="notif-push">Push notifications</label>
  </div>
</fieldset>

<!-- Select All (Indeterminate) -->
<div class="checkbox-field">
  <input type="checkbox" id="select-all" aria-label="Select all rows" />
  <label for="select-all">Select all</label>
</div>

<script>
  // Set indeterminate state (no HTML attribute for this)
  const selectAll = document.getElementById("select-all");
  selectAll.indeterminate = true;
</script>
tsx
import { useRef, useEffect, forwardRef, type InputHTMLAttributes } from "react";

interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> {
  label: string;
  description?: string;
  error?: string;
  indeterminate?: boolean;
  checked?: boolean;
  onChange?: (checked: boolean) => void;
  size?: "sm" | "md" | "lg";
}

const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
  (
    {
      label,
      description,
      error,
      indeterminate = false,
      checked,
      onChange,
      size = "md",
      id,
      disabled,
      className = "",
      ...props
    },
    ref,
  ) => {
    const internalRef = useRef<HTMLInputElement>(null);
    const checkboxRef = (ref as React.RefObject<HTMLInputElement>) || internalRef;

    useEffect(() => {
      if (checkboxRef.current) {
        checkboxRef.current.indeterminate = indeterminate;
      }
    }, [indeterminate]);

    const inputId = id || `checkbox-${label.toLowerCase().replace(/\s+/g, "-")}`;

    return (
      <div className={`checkbox-field checkbox-${size} ${error ? "has-error" : ""} ${className}`}>
        <input
          ref={checkboxRef}
          type="checkbox"
          id={inputId}
          checked={checked}
          disabled={disabled}
          aria-invalid={!!error || undefined}
          aria-describedby={
            [description && `${inputId}-desc`, error && `${inputId}-error`]
              .filter(Boolean)
              .join(" ") || undefined
          }
          onChange={(e) => onChange?.(e.target.checked)}
          {...props}
        />
        <label htmlFor={inputId}>
          {label}
          {description && (
            <span id={`${inputId}-desc`} className="checkbox-description">
              {description}
            </span>
          )}
        </label>
        {error && (
          <span id={`${inputId}-error`} className="checkbox-error" role="alert">
            {error}
          </span>
        )}
      </div>
    );
  },
);

Checkbox.displayName = "Checkbox";
export default Checkbox;

// Usage
<Checkbox
  label="Subscribe to newsletter"
  description="We send updates once a week."
  checked={subscribed}
  onChange={setSubscribed}
/>

<Checkbox
  label="Select all"
  indeterminate={someSelected && !allSelected}
  checked={allSelected}
  onChange={handleSelectAll}
/>

Design Systems

Cross-System Comparison

FeatureMaterial 3Shadcn/uiRadixAnt Design
ElementCustom <div> with ripple<button role="checkbox"> via RadixPrimitive <button role="checkbox">Custom <span> wrapping <input>
IndeterminateSupported (dash icon)Supported via data-state="indeterminate"checked="indeterminate" propindeterminate prop
AnimationRipple effect on toggleCSS transition on check iconBYO animationAnt Motion check animation
Group componentFormGroup wrapperManual with <fieldset>No group primitiveCheckbox.Group with options array
Tri-state handlingManualManualonCheckedChange(checked: boolean | "indeterminate")Manual with onChange
Error stateVia FormField wrapperManual stylingNot built-inVia Form.Item wrapper
AccessibilityCustom ARIARadix handles role + aria-checkedFull — role="checkbox", aria-checked, keyboardNative <input> under the hood

Notable Approaches

Radix Checkbox is a <button> with role="checkbox" rather than a native <input type="checkbox">. This is intentional — it gives full styling control (native checkboxes are notoriously hard to style consistently across browsers) while maintaining accessibility. The trade-off: it doesn't participate in native form submission without a hidden <input> companion, which Radix handles internally.

Material 3 adds a ripple effect to checkbox interactions, consistent with their touch feedback philosophy. The ripple radiates from the point of click — a nice detail for touch interfaces. They also support an "error" state where the checkbox border turns red, which is less common but useful for required agreement checkboxes.

Ant Design's Checkbox.Group is remarkably convenient — you pass an options array and get a fully managed group with a value array and onChange callback. This declarative approach reduces boilerplate significantly for common "pick multiple" forms.

Native vs. custom: The eternal debate. Native <input type="checkbox"> is accessible by default and participates in form submission, but is nearly impossible to style consistently. Custom implementations (<button role="checkbox"> or <div role="checkbox">) offer full visual control but require manual accessibility work. Most modern design systems opt for custom with careful ARIA implementation. The native accent-color CSS property (supported in all modern browsers as of 2026) provides a middle ground — it lets you change the checkbox color while keeping native behavior.

Data InputCheckTick Box