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

Install

$npx react-principles add checkbox
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
Size
03

Code Snippet

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

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

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

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

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

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

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

04

Copy-Paste (Single File)

Checkbox.tsx
import { useRef, useEffect } from "react";
import { cn } from "@/lib/utils";

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

export 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 BOX_SIZES: Record<CheckboxSize, string> = {
  sm: "h-4 w-4",
  md: "h-5 w-5",
  lg: "h-6 w-6",
};

const ICON_SIZES: Record<CheckboxSize, string> = {
  sm: "h-2.5 w-2.5",
  md: "h-3 w-3",
  lg: "h-3.5 w-3.5",
};

const LABEL_SIZES: Record<CheckboxSize, string> = {
  sm: "text-xs",
  md: "text-sm",
  lg: "text-base",
};

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg className={className} viewBox="0 0 12 12" fill="none">
      <path d="M2 6l3 3 5-5" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function MinusIcon({ className }: { className?: string }) {
  return (
    <svg className={className} viewBox="0 0 12 12" fill="none">
      <path d="M2.5 6h7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" />
    </svg>
  );
}

export function Checkbox({
  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
              ? "bg-primary border-primary"
              : "bg-white dark:bg-[#0d1117] border-slate-300 dark:border-slate-600",
            !disabled && !isFilled && "hover:border-primary",
          )}
        >
          {isChecked && <CheckIcon className={cn("text-white", ICON_SIZES[size])} />}
          {indeterminate && !isChecked && <MinusIcon className={cn("text-white", ICON_SIZES[size])} />}
        </div>
      </div>

      {(label ?? description) && (
        <div className="min-w-0">
          {label && (
            <span className={cn("block font-medium text-slate-900 dark:text-white leading-tight", LABEL_SIZES[size])}>
              {label}
            </span>
          )}
          {description && (
            <p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5 leading-relaxed">
              {description}
            </p>
          )}
        </div>
      )}
    </label>
  );
}
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