Checkbox

A checkbox component for selecting multiple options from a list. Supports labels, controlled and uncontrolled modes.

Installation

Terminal
ReactTailwind
TSX
npx @uiblox/cli add checkbox

Visual Variations

Toggle between three different visual styles: default (square), rounded (circular), and filled (background).

Accept terms

Click to toggle

Newsletter

Pre-selected option

Locked option

Non-interactive

Basic Usage

Copy & paste ready
import { Checkbox } from "@/components/ui";

const [checked, setChecked] = useState(false);

<Checkbox
  checked={checked}
  onCheckedChange={setChecked}
  label="Accept terms and conditions"
/>

States

Copy & paste ready
<Checkbox label="Unchecked" />
<Checkbox label="Checked" defaultChecked />
<Checkbox label="Disabled" disabled />
<Checkbox label="Disabled Checked" disabled defaultChecked />

Without Label

Copy & paste ready
<Checkbox />
<Checkbox defaultChecked />

Form Example

Notification Preferences

Copy & paste ready
<div className="space-y-4 p-4 border rounded-lg">
  <h3 className="font-medium">Notification Preferences</h3>
  <div className="space-y-2">
    <Checkbox label="Email notifications" defaultChecked />
    <Checkbox label="Push notifications" />
    <Checkbox label="SMS notifications" />
    <Checkbox label="Marketing emails" />
  </div>
</div>

Accessibility

The Checkbox component uses a native input for full accessibility support, with custom styling that maintains all assistive technology features.

Keyboard Support

  • Space - Toggle checkbox state
  • Tab - Move focus to/from checkbox

Built-in Features

  • Native checkbox with sr-only for screen readers
  • Automatic label association via htmlFor
  • Focus-visible ring for keyboard users

Accessible Checkbox Patterns

// Basic accessible checkbox (label prop handles association)
<Checkbox
  label="I agree to the terms and conditions"
/>

// Required checkbox with description
<div className="space-y-1">
  <Checkbox
    id="terms"
    label="Accept terms"
    aria-describedby="terms-description"
    aria-required="true"
  />
  <p id="terms-description" className="text-sm text-[#94a3b8] ml-6">
    You must accept our terms to continue.
  </p>
</div>

// Checkbox group with fieldset for screen readers
<fieldset>
  <legend className="text-sm font-medium text-white mb-3">
    Notification Preferences
  </legend>
  <div className="space-y-2" role="group" aria-label="Notification options">
    <Checkbox label="Email notifications" name="notifications" />
    <Checkbox label="Push notifications" name="notifications" />
    <Checkbox label="SMS notifications" name="notifications" />
  </div>
</fieldset>

// Error state
<div className="space-y-1">
  <Checkbox
    label="I accept the privacy policy"
    aria-invalid="true"
    aria-describedby="checkbox-error"
  />
  <p id="checkbox-error" className="text-sm text-red-400 ml-6" role="alert">
    You must accept the privacy policy to continue.
  </p>
</div>

// Without visible label (use aria-label)
<Checkbox
  aria-label="Select row for John Doe"
/>

Best Practices

  • • Always provide a label (visible or via aria-label)
  • • Group related checkboxes with <fieldset> and <legend>
  • • Use aria-describedby to link helper text or errors
  • • Clicking the label should toggle the checkbox

Props

PropTypeDefaultDescription
checkedboolean-Controlled checked state
defaultCheckedbooleanfalseDefault checked state for uncontrolled usage
onCheckedChange(checked: boolean) => void-Callback when checked state changes
labelstring-Label text displayed next to checkbox
disabledbooleanfalseWhether the checkbox is disabled

Source Code

Copy this code into src/components/ui/checkbox.tsx:

checkbox.tsx
ReactTailwind
TSX
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange" | "type"> {
  onCheckedChange?: (checked: boolean) => void;
  label?: string;
}

const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
  ({ className, checked, defaultChecked, onCheckedChange, label, disabled, id, ...props }, ref) => {
    const [internalChecked, setInternalChecked] = React.useState(defaultChecked ?? false);
    const isChecked = checked ?? internalChecked;
    const generatedId = React.useId();
    const inputId = id ?? generatedId;

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      const newChecked = e.target.checked;
      if (checked === undefined) {
        setInternalChecked(newChecked);
      }
      onCheckedChange?.(newChecked);
    };

    return (
      <div className="flex items-center">
        <div className="relative flex items-center">
          <input
            ref={ref}
            type="checkbox"
            id={inputId}
            checked={isChecked}
            onChange={handleChange}
            disabled={disabled}
            className="sr-only peer"
            {...props}
          />
          <div
            className={cn(
              "h-4 w-4 shrink-0 rounded border border-gray-300 dark:border-gray-600 transition-colors",
              "peer-focus-visible:ring-2 peer-focus-visible:ring-primary-500 peer-focus-visible:ring-offset-2",
              "peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
              isChecked && "bg-primary-600 border-primary-600",
              className
            )}
          >
            {isChecked && (
              <svg className="h-full w-full text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
                <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
              </svg>
            )}
          </div>
        </div>
        {label && (
          <label
            htmlFor={inputId}
            className={cn(
              "ml-2 text-sm font-medium text-gray-700 dark:text-gray-200",
              disabled && "cursor-not-allowed opacity-50"
            )}
          >
            {label}
          </label>
        )}
      </div>
    );
  }
);
Checkbox.displayName = "Checkbox";

export { Checkbox };