Checkbox List Toggle
Original · freecheckbox list with select-all
byWeb InnoventixReact + Tailwind
tglcheckboxlisttoggles
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/tgl-checkbox-list.jsontgl-checkbox-list.tsx
"use client";
import { useEffect, useId, useMemo, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type ToneKey = "indigo" | "emerald" | "violet";
type BoxState = "on" | "off" | "mixed";
type Tone = {
fill: string;
ring: string;
bar: string;
track: string;
text: string;
chip: string;
btn: string;
};
const TONES: Record<ToneKey, Tone> = {
indigo: {
fill: "border-indigo-500 bg-indigo-500 dark:border-indigo-400 dark:bg-indigo-500",
ring: "peer-focus-visible:ring-indigo-500 dark:peer-focus-visible:ring-indigo-400",
bar: "bg-indigo-500 dark:bg-indigo-400",
track: "bg-indigo-100 dark:bg-indigo-500/20",
text: "text-indigo-600 dark:text-indigo-400",
chip: "bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-500/30",
btn: "bg-indigo-600 hover:bg-indigo-500 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-400",
},
emerald: {
fill: "border-emerald-500 bg-emerald-500 dark:border-emerald-400 dark:bg-emerald-500",
ring: "peer-focus-visible:ring-emerald-500 dark:peer-focus-visible:ring-emerald-400",
bar: "bg-emerald-500 dark:bg-emerald-400",
track: "bg-emerald-100 dark:bg-emerald-500/20",
text: "text-emerald-600 dark:text-emerald-400",
chip: "bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-500/30",
btn: "bg-emerald-600 hover:bg-emerald-500 focus-visible:outline-emerald-600 dark:bg-emerald-500 dark:hover:bg-emerald-400 dark:focus-visible:outline-emerald-400",
},
violet: {
fill: "border-violet-500 bg-violet-500 dark:border-violet-400 dark:bg-violet-500",
ring: "peer-focus-visible:ring-violet-500 dark:peer-focus-visible:ring-violet-400",
bar: "bg-violet-500 dark:bg-violet-400",
track: "bg-violet-100 dark:bg-violet-500/20",
text: "text-violet-600 dark:text-violet-400",
chip: "bg-violet-50 text-violet-700 ring-violet-200 dark:bg-violet-500/10 dark:text-violet-300 dark:ring-violet-500/30",
btn: "bg-violet-600 hover:bg-violet-500 focus-visible:outline-violet-600 dark:bg-violet-500 dark:hover:bg-violet-400 dark:focus-visible:outline-violet-400",
},
};
const AVATAR: Record<string, string> = {
Design: "bg-sky-100 text-sky-700 dark:bg-sky-500/20 dark:text-sky-300",
Engineering: "bg-violet-100 text-violet-700 dark:bg-violet-500/20 dark:text-violet-300",
Product: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/20 dark:text-emerald-300",
Marketing: "bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-300",
};
const ROLE_CHIP: Record<string, string> = {
Design: "bg-sky-50 text-sky-700 ring-sky-200 dark:bg-sky-500/10 dark:text-sky-300 dark:ring-sky-500/30",
Engineering: "bg-violet-50 text-violet-700 ring-violet-200 dark:bg-violet-500/10 dark:text-violet-300 dark:ring-violet-500/30",
Product: "bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-500/30",
Marketing: "bg-amber-50 text-amber-700 ring-amber-200 dark:bg-amber-500/10 dark:text-amber-300 dark:ring-amber-500/30",
};
const KEYFRAMES = `
@keyframes tglcl-pop { 0% { transform: scale(.55); } 60% { transform: scale(1.12); } 100% { transform: scale(1); } }
@keyframes tglcl-rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.tglcl-pop-anim { animation: tglcl-pop .26s ease-out both; }
.tglcl-rise { animation: tglcl-rise .55s cubic-bezier(.22,1,.36,1) both; }
@media (prefers-reduced-motion: reduce) {
.tglcl-pop-anim, .tglcl-rise { animation: none !important; }
}
`;
function cn(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(" ");
}
function initials(name: string): string {
return name
.split(" ")
.map((w) => w.charAt(0))
.slice(0, 2)
.join("")
.toUpperCase();
}
type Selection = {
has: (id: string) => boolean;
count: number;
all: boolean;
none: boolean;
mixed: boolean;
toggle: (id: string) => void;
setAll: (on: boolean) => void;
};
function useSelection(ids: string[], initial: string[]): Selection {
const [selected, setSelected] = useState<Set<string>>(() => new Set(initial));
const toggle = (id: string) => {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
const setAll = (on: boolean) => {
setSelected(on ? new Set(ids) : new Set<string>());
};
const count = selected.size;
const all = count === ids.length && count > 0;
const none = count === 0;
return {
has: (id: string) => selected.has(id),
count,
all,
none,
mixed: !all && !none,
toggle,
setAll,
};
}
function CheckIcon({ reduce }: { reduce: boolean }) {
return (
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className={cn("size-3.5 text-white", !reduce && "tglcl-pop-anim")}
>
<motion.path
d="M4.5 10.5 8.25 14.25 15.5 6.25"
stroke="currentColor"
strokeWidth={2.4}
strokeLinecap="round"
strokeLinejoin="round"
initial={reduce ? false : { pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: reduce ? 0 : 0.24, ease: "easeOut" }}
/>
</svg>
);
}
function Box({
state,
tone,
reduce,
className,
}: {
state: BoxState;
tone: ToneKey;
reduce: boolean;
className?: string;
}) {
const t = TONES[tone];
return (
<span
aria-hidden="true"
className={cn(
"pointer-events-none relative grid size-5 shrink-0 place-items-center rounded-[6px] border transition-colors duration-150",
"peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-900",
t.ring,
state === "off"
? "border-slate-300 bg-white group-hover:border-slate-400 dark:border-slate-600 dark:bg-slate-800 dark:group-hover:border-slate-500"
: t.fill,
className,
)}
>
{state === "on" && <CheckIcon reduce={reduce} />}
{state === "mixed" && <span className="h-[2px] w-2.5 rounded-full bg-white" />}
</span>
);
}
function MasterCheckbox({
label,
text,
state,
tone,
reduce,
onToggle,
}: {
label: string;
text: string;
state: BoxState;
tone: ToneKey;
reduce: boolean;
onToggle: (on: boolean) => void;
}) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (ref.current) ref.current.indeterminate = state === "mixed";
}, [state]);
return (
<label className="group inline-flex cursor-pointer select-none items-center gap-2.5">
<input
ref={ref}
type="checkbox"
className="peer sr-only"
checked={state === "on"}
onChange={(e) => onToggle(e.target.checked)}
aria-label={label}
/>
<Box state={state} tone={tone} reduce={reduce} />
<span className="text-sm font-semibold text-slate-700 dark:text-slate-200">{text}</span>
</label>
);
}
function CheckboxRow({
label,
description,
tone,
checked,
strike,
reduce,
onToggle,
}: {
label: string;
description?: string;
tone: ToneKey;
checked: boolean;
strike: boolean;
reduce: boolean;
onToggle: (next: boolean) => void;
}) {
return (
<li>
<label className="group flex cursor-pointer select-none items-start gap-3 rounded-xl px-3 py-2.5 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/60">
<input
type="checkbox"
className="peer sr-only"
checked={checked}
onChange={(e) => onToggle(e.target.checked)}
/>
<Box state={checked ? "on" : "off"} tone={tone} reduce={reduce} className="mt-0.5" />
<span className="min-w-0">
<span
className={cn(
"block text-sm font-medium transition-colors",
checked && strike
? "text-slate-400 line-through dark:text-slate-500"
: "text-slate-800 dark:text-slate-100",
)}
>
{label}
</span>
{description ? (
<span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">
{description}
</span>
) : null}
</span>
</label>
</li>
);
}
type Item = { id: string; label: string; description?: string };
function ChecklistCard({
title,
subtitle,
items,
tone,
strike,
reduce,
initial,
delay,
}: {
title: string;
subtitle: string;
items: Item[];
tone: ToneKey;
strike: boolean;
reduce: boolean;
initial: string[];
delay: number;
}) {
const ids = useMemo(() => items.map((i) => i.id), [items]);
const sel = useSelection(ids, initial);
const t = TONES[tone];
const total = items.length;
const pct = total === 0 ? 0 : Math.round((sel.count / total) * 100);
const barId = useId();
const masterState: BoxState = sel.all ? "on" : sel.mixed ? "mixed" : "off";
return (
<div
className="tglcl-rise rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900"
style={{ animationDelay: `${delay}ms` }}
>
<div className="mb-3 flex items-start justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-slate-900 dark:text-white">{title}</h3>
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">{subtitle}</p>
</div>
<span
className={cn(
"shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold tabular-nums ring-1 ring-inset",
t.chip,
)}
>
{sel.count}/{total}
</span>
</div>
<div className="flex items-center justify-between border-y border-slate-100 py-2 dark:border-slate-800">
<MasterCheckbox
label={`Select all — ${title}`}
text="Select all"
state={masterState}
tone={tone}
reduce={reduce}
onToggle={sel.setAll}
/>
<button
type="button"
onClick={() => sel.setAll(false)}
disabled={sel.none}
className="rounded-md px-2 py-1 text-xs font-medium text-slate-500 transition hover:text-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 disabled:pointer-events-none disabled:opacity-40 dark:text-slate-400 dark:hover:text-slate-200 dark:focus-visible:ring-slate-500"
>
Clear
</button>
</div>
<div className="mt-3">
<div
role="progressbar"
aria-valuenow={sel.count}
aria-valuemin={0}
aria-valuemax={total}
aria-labelledby={barId}
className={cn("h-1.5 w-full overflow-hidden rounded-full", t.track)}
>
<div
className={cn("h-full rounded-full transition-[width] duration-300 ease-out", t.bar)}
style={{ width: `${pct}%` }}
/>
</div>
</div>
<ul role="list" className="mt-2">
{items.map((it) => (
<CheckboxRow
key={it.id}
label={it.label}
description={it.description}
tone={tone}
checked={sel.has(it.id)}
strike={strike}
reduce={reduce}
onToggle={() => sel.toggle(it.id)}
/>
))}
</ul>
<p
id={barId}
aria-live="polite"
className="mt-1 px-3 text-xs text-slate-500 dark:text-slate-400"
>
<span className={cn("font-semibold tabular-nums", t.text)}>{sel.count}</span> of {total} selected
</p>
</div>
);
}
function PlusIcon() {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="size-4">
<path
d="M10 4.5v11M4.5 10h11"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
/>
</svg>
);
}
type Member = { id: string; name: string; email: string; role: string };
const RELEASE: Item[] = [
{ id: "tests", label: "Run the full test suite", description: "1,284 tests across 96 files" },
{ id: "changelog", label: "Update CHANGELOG.md", description: "Everything since v2.3.1" },
{ id: "version", label: "Bump version in package.json", description: "2.3.1 → 2.4.0" },
{ id: "tag", label: "Tag the release on GitHub", description: "Signed annotated tag" },
{ id: "slack", label: "Announce in #engineering", description: "Slack, with release notes" },
{ id: "deploy", label: "Deploy to production", description: "Vercel · primary region" },
];
const NOTIFY: Item[] = [
{ id: "product", label: "Product updates", description: "New features, improvements, and fixes" },
{ id: "security", label: "Security alerts", description: "Sign-ins from new devices and password changes" },
{ id: "billing", label: "Billing receipts", description: "Monthly invoices and payment confirmations" },
{ id: "digest", label: "Weekly digest", description: "A Monday summary of your workspace activity" },
{ id: "events", label: "Community & events", description: "Webinars, meetups, and beta invitations" },
];
const TEAM: Member[] = [
{ id: "ava", name: "Ava Chen", email: "ava.chen@northwind.io", role: "Design" },
{ id: "marcus", name: "Marcus Reed", email: "m.reed@northwind.io", role: "Engineering" },
{ id: "priya", name: "Priya Nair", email: "priya.nair@northwind.io", role: "Product" },
{ id: "leo", name: "Leo Fischer", email: "leo.fischer@northwind.io", role: "Marketing" },
{ id: "sofia", name: "Sofia Duarte", email: "s.duarte@northwind.io", role: "Engineering" },
];
const TEAM_IDS = TEAM.map((m) => m.id);
export default function TglCheckboxList() {
const reduce = !!useReducedMotion();
const team = useSelection(TEAM_IDS, ["ava", "marcus"]);
const headingId = useId();
const teamMasterState: BoxState = team.all ? "on" : team.mixed ? "mixed" : "off";
return (
<section
aria-labelledby={headingId}
className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 sm:px-6 sm:py-24 dark:bg-slate-950"
>
<style>{KEYFRAMES}</style>
<div className="mx-auto max-w-6xl">
<header className="mb-10 max-w-2xl">
<span className="inline-flex items-center gap-2 rounded-full bg-slate-200/70 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-slate-600 dark:bg-slate-800 dark:text-slate-300">
Toggles
</span>
<h2
id={headingId}
className="mt-4 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-white"
>
Checkbox lists with select-all
</h2>
<p className="mt-3 text-sm leading-relaxed text-slate-600 sm:text-base dark:text-slate-400">
A master control drives the whole set and shows a true mixed (indeterminate) state
when only some rows are checked. Fully keyboard-operable, screen-reader labelled, and
built on real native checkboxes.
</p>
</header>
<div className="grid gap-6 lg:grid-cols-2">
<ChecklistCard
title="Release checklist"
subtitle="Shipping v2.4.0 — check items off as you go"
items={RELEASE}
tone="indigo"
strike
reduce={reduce}
initial={["tests", "changelog"]}
delay={0}
/>
<ChecklistCard
title="Email notifications"
subtitle="Pick which emails Northwind can send you"
items={NOTIFY}
tone="emerald"
strike={false}
reduce={reduce}
initial={["security", "billing"]}
delay={60}
/>
</div>
<div
className="tglcl-rise mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900"
style={{ animationDelay: "120ms" }}
>
<div className="mb-3 flex flex-wrap items-center justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-slate-900 dark:text-white">
Invite your team
</h3>
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
Choose who gets access to the Northwind workspace.
</p>
</div>
<span
className={cn(
"shrink-0 rounded-full px-2.5 py-1 text-xs font-semibold tabular-nums ring-1 ring-inset",
TONES.violet.chip,
)}
>
{team.count}/{TEAM.length} selected
</span>
</div>
<div className="overflow-hidden rounded-xl border border-slate-200 dark:border-slate-800">
<div className="flex items-center gap-3 border-b border-slate-200 bg-slate-50 px-3 py-2.5 dark:border-slate-800 dark:bg-slate-800/50">
<MasterCheckbox
label="Select all team members"
text="Select all"
state={teamMasterState}
tone="violet"
reduce={reduce}
onToggle={team.setAll}
/>
<span className="ml-auto hidden text-xs font-medium uppercase tracking-wide text-slate-400 sm:inline dark:text-slate-500">
Role
</span>
</div>
<ul role="list" className="divide-y divide-slate-100 dark:divide-slate-800">
{TEAM.map((m) => {
const checked = team.has(m.id);
return (
<li key={m.id}>
<label className="group flex cursor-pointer select-none items-center gap-3 px-3 py-3 transition-colors hover:bg-slate-50 dark:hover:bg-slate-800/50">
<input
type="checkbox"
className="peer sr-only"
checked={checked}
onChange={() => team.toggle(m.id)}
aria-label={`Invite ${m.name}, ${m.role}`}
/>
<Box state={checked ? "on" : "off"} tone="violet" reduce={reduce} />
<span
aria-hidden="true"
className={cn(
"grid size-9 shrink-0 place-items-center rounded-full text-xs font-semibold",
AVATAR[m.role],
)}
>
{initials(m.name)}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-slate-800 dark:text-slate-100">
{m.name}
</span>
<span className="block truncate text-xs text-slate-500 dark:text-slate-400">
{m.email}
</span>
</span>
<span
className={cn(
"hidden shrink-0 rounded-full px-2.5 py-1 text-xs font-medium ring-1 ring-inset sm:inline-block",
ROLE_CHIP[m.role],
)}
>
{m.role}
</span>
</label>
</li>
);
})}
</ul>
</div>
<div className="mt-4 flex flex-wrap items-center justify-between gap-3">
<p aria-live="polite" className="text-xs text-slate-500 dark:text-slate-400">
{team.none ? (
"No one selected yet."
) : (
<>
<span className="font-semibold text-violet-600 dark:text-violet-400">
{team.count}
</span>{" "}
{team.count === 1 ? "person" : "people"} will be invited.
</>
)}
</p>
<button
type="button"
disabled={team.none}
className={cn(
"inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-semibold text-white shadow-sm transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-40",
TONES.violet.btn,
)}
>
<PlusIcon />
Invite {team.count > 0 ? `${team.count} ` : ""}
{team.count === 1 ? "member" : "members"}
</button>
</div>
</div>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Checkbox Set Toggle
Originaldefault/checked/indeterminate/disabled checkboxes

Checkbox Custom Toggle
Originalcustom animated tick checkboxes

Checkbox Card Toggle
Originalselectable card checkboxes

Checkbox Indeterminate Toggle
Originalparent/child indeterminate checkboxes

Radio Set Toggle
Originalstyled radio button group

Radio Cards Toggle
Originalselectable radio cards

Radio Segmented Toggle
Originalsegmented control radios

Switch Basic Toggle
Originalbasic on/off switches in sizes

Switch iOS Toggle
OriginaliOS-style switches
Switch Icons Toggle
Originalswitches with sun/moon style icons

Switch Labels Toggle
Originalswitches with on/off labels inside

Switch Group Toggle
Originala settings list of switches

