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-groupOr install the full npm package:
npm install bigbulluiUsage
Open in StackBlitzimport { 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.tsxZero 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
| Prop | Type | Description |
|---|---|---|
| options | CheckboxGroupOption[] | Available options. |
| value | string[] | Controlled selected values. |
| defaultValue | string[] | Uncontrolled initial values. |
| onValueChange | (value: string[]) => void | Change handler. |