GitHub

Checkbox

A binary or indeterminate selection control. Supports label, description, three sizes, and an indeterminate state for select-all patterns.

AccessibleDark ModeIndeterminate3 SizesCustom Visual
01

Theme Preview

All four states — unchecked, checked, indeterminate, and disabled — rendered with forced light and dark styling for direct comparison.

Light
Accept termsUnchecked
Email notificationsChecked
All categoriesIndeterminate
Archived itemsDisabled
Dark
Accept termsUnchecked
Email notificationsChecked
All categoriesIndeterminate
Archived itemsDisabled
02

Live Demo

Size
03

Code Snippet

src/ui/Checkbox.tsx
import { Checkbox } from "@/ui/Checkbox";

// Basic
<Checkbox.Root label="Accept terms and conditions" />

// Controlled
<Checkbox.Root
  checked={isChecked}
  onChange={setIsChecked}
  label="Email notifications"
  description="Receive updates via email."
/>

// Indeterminate (select-all pattern)
<Checkbox.Root
  checked={allSelected}
  indeterminate={someSelected && !allSelected}
  onChange={handleSelectAll}
  label="Select all"
/>

// States
<Checkbox.Root checked label="Checked" />
<Checkbox.Root indeterminate label="Indeterminate" />
<Checkbox.Root disabled label="Disabled" />
<Checkbox.Root checked disabled label="Checked + disabled" />

// Sizes
<Checkbox.Root size="sm" label="Small" />
<Checkbox.Root size="md" label="Medium" />
<Checkbox.Root size="lg" label="Large" />

Backward compatible: API lama <Checkbox /> masih didukung, tapi docs sekarang pakai <Checkbox.Root />.

04

Copy-Paste (Single File)

Checkbox.tsx
import { useEffect, useRef } from "react";

type CheckboxSize = "sm" | "md" | "lg";

interface CheckboxProps {
  checked?: boolean;
  defaultChecked?: boolean;
  indeterminate?: boolean;
  disabled?: boolean;
  size?: CheckboxSize;
  label?: string;
  description?: string;
  id?: string;
  name?: string;
  onChange?: (checked: boolean) => void;
  className?: string;
}

const cn = (...classes: Array<string | undefined | false>) => classes.filter(Boolean).join(" ");

const BOX_SIZES: Record<CheckboxSize, string> = {
  sm: "h-4 w-4",
  md: "h-5 w-5",
  lg: "h-6 w-6",
};

function CheckboxRoot({ checked, defaultChecked, indeterminate = false, disabled = false, size = "md", label, description, id, name, onChange, className }: CheckboxProps) {
  const inputRef = useRef<HTMLInputElement>(null);
  useEffect(() => {
    if (inputRef.current) inputRef.current.indeterminate = indeterminate;
  }, [indeterminate]);

  const isChecked = checked ?? false;
  const isFilled = isChecked || indeterminate;

  return (
    <label className={cn("inline-flex items-start gap-3", disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer", className)}>
      <div className="relative mt-0.5 shrink-0">
        <input ref={inputRef} type="checkbox" id={id} name={name} checked={checked} defaultChecked={defaultChecked} disabled={disabled} onChange={(e) => onChange?.(e.target.checked)} className="sr-only" />
        <div className={cn("flex items-center justify-center rounded-sm border-2 transition-all", BOX_SIZES[size], isFilled ? "border-primary bg-primary text-white" : "border-slate-300 bg-white dark:border-slate-600 dark:bg-[#0d1117]")}>
          {isChecked && <span className="text-[10px]"></span>}
          {indeterminate && !isChecked && <span className="text-[10px]"></span>}
        </div>
      </div>
      {(label ?? description) && (
        <div className="min-w-0">
          {label && <span className="block text-sm font-medium leading-tight text-slate-900 dark:text-white">{label}</span>}
          {description && <p className="mt-0.5 text-xs leading-relaxed text-slate-500 dark:text-slate-400">{description}</p>}
        </div>
      )}
    </label>
  );
}

type CheckboxCompound = typeof CheckboxRoot & { Root: typeof CheckboxRoot };
export const Checkbox = Object.assign(CheckboxRoot, { Root: CheckboxRoot }) as CheckboxCompound;
05

Props

PropTypeDefaultDescription
checkedbooleanControlled checked state.
defaultCheckedbooleanfalseUncontrolled initial checked state.
indeterminatebooleanfalseShows a dash — used for partial selection in select-all patterns.
disabledbooleanfalsePrevents interaction and reduces opacity.
size"sm" | "md" | "lg""md"Controls box size and label font size.
labelstringClickable text label rendered beside the checkbox.
descriptionstringSecondary muted text displayed below the label.
onChange(checked: boolean) => voidCallback fired with the new boolean value.
id / namestringForwarded to the underlying <input> element.
react-principles