Componentform1.13 KB gzipZero dependencies

Checkbox Group

Checkbox options with a select-all row (indeterminate state), animated check fills, and staggered entrance.

Preview

Live Preview
Select all
Option A
Option B
Option C

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add checkbox-group

Or install the full npm package:

npm install bigbullui
import { CheckboxGroup } from "@/components/ui/checkbox-group";

<CheckboxGroup options={options} value={value} onValueChange={setValue} />

Usage guidance

Use Checkbox Group when a ticket stub surface needs Checkbox options with a select-all row (indeterminate state), animated check fills, and staggered entrance. Keep props minimal and prefer composition with sibling stubs.

Avoid Checkbox Group for long form flows or dense data grids where a dedicated layout block fits better.

Source code

checkbox-group.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

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

export type CheckboxGroupOption = {
  value: string;
  label: string;
  disabled?: boolean;
};

export type CheckboxGroupProps = {
  value?: string[];
  defaultValue?: string[];
  onValueChange?: (value: string[]) => void;
  options: CheckboxGroupOption[];
  className?: string;
};

const CheckboxGroup = React.forwardRef<HTMLDivElement, CheckboxGroupProps>(
  ({ value = [], defaultValue = [], onValueChange, options, className }, ref) => {
    const [internal, setInternal] = React.useState<string[]>(() => defaultValue);
    const [showAll, setShowAll] = React.useState(false);

    // Sync from controlled prop
    React.useEffect(() => {
      if (value !== undefined) {
        setInternal(value);
      }
    }, [value]);

    const toggledValues = options
      .filter((opt) => !opt.disabled)
      .reduce((acc, opt) => {
        if (internal.includes(opt.value)) {
          acc.push(opt.value);
        }
        return acc;
      }, [] as string[]);

    const handleChange = (value: string) => {
      if (toggledValues.includes(value)) {
        setInternal(toggledValues.filter((v) => v !== value));
      } else {
        setInternal([...toggledValues, value]);
      }
      onValueChange?.(toggledValues);
    };

    const handleSelectAll = () => {
      setShowAll(!showAll);
      const allValues = options.filter((opt) => !opt.disabled).map((opt) => opt.value);
      setInternal(allValues);
      onValueChange?.(allValues);
    };

    return (
      <div
        ref={ref}
        className={cn(
          "space-y-2",
          className
        )}
      >
        {/* Select all row */}
        <div className="group flex items-start">
          <input
            type="checkbox"
            checked={showAll}
            onChange={handleSelectAll}
            className={cn(
              "w-4 h-4 rounded border border-input bg-background shrink-0 cursor-pointer flex-shrink-0",
              "motion-reduce:transition-none"
            )}
            aria-label="Select all"
          />
          <span className="ms-2 text-sm flex-1 uppercase tracking-[0.15em] text-muted-foreground">
            {showAll ? "Deselect all" : "Select all"}
          </span>
        </div>

        {/* Options with staggered entrance */}
        {options.map((opt, index) => (
          <div
            key={opt.value}
            className="flex items-start space-x-2"
            style={{
              animation: `stamp-in 0.3s ease-out ${index * 50}ms both`,
            }}
          >
            <input
              type="checkbox"
              checked={internal.includes(opt.value)}
              onChange={() => handleChange(opt.value)}
              disabled={opt.disabled}
              className={cn(
                "w-4 h-4 rounded border border-input bg-background shrink-0 cursor-pointer flex-shrink-0",
                "motion-reduce:transition-none"
              )}
            />
            <span className="ms-2 text-sm flex-1 uppercase tracking-[0.15em]">
              {opt.label}
            </span>
          </div>
        ))}

        {options.length === 0 && (
          <p className="text-sm text-muted-foreground">No options available</p>
        )}
      </div>
    );
  }
);
CheckboxGroup.displayName = "CheckboxGroup";

export { CheckboxGroup };

Props

PropTypeDescription
optionsCheckboxGroupOption[]Available options.
valuestring[]Controlled selected values.
defaultValuestring[]Uncontrolled initial values.
onValueChange(value: string[]) => voidChange handler.