Checkbox Set Toggle
Original · freedefault/checked/indeterminate/disabled checkboxes
byWeb InnoventixReact + Tailwind
tglcheckboxsettoggles
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-set.jsontgl-checkbox-set.tsx
"use client";
import { useEffect, useId, useRef, useState } from "react";
import type { ReactNode } from "react";
import { motion, useReducedMotion } from "motion/react";
type CheckboxProps = {
label: ReactNode;
description?: string;
checked: boolean;
indeterminate?: boolean;
disabled?: boolean;
onChange?: (checked: boolean) => void;
};
function Checkbox({
label,
description,
checked,
indeterminate = false,
disabled = false,
onChange,
}: CheckboxProps) {
const reduce = useReducedMotion();
const inputRef = useRef<HTMLInputElement | null>(null);
const autoId = useId();
const descId = description ? `${autoId}-desc` : undefined;
const isIndeterminate = indeterminate && !checked;
const active = checked || isIndeterminate;
useEffect(() => {
if (inputRef.current) {
inputRef.current.indeterminate = isIndeterminate;
}
}, [isIndeterminate]);
const boxClasses = [
"flex h-5 w-5 shrink-0 items-center justify-center rounded-[7px] border",
"transition-[background-color,border-color,box-shadow] duration-200 ease-out",
active
? "border-indigo-500 bg-indigo-500 text-white dark:border-indigo-400 dark:bg-indigo-500"
: "border-slate-300 bg-white text-transparent dark:border-slate-600 dark:bg-slate-900",
!disabled && !active
? "peer-hover:border-indigo-400 dark:peer-hover:border-indigo-500"
: "",
"peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500/60 peer-focus-visible:ring-offset-2",
"peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-900",
]
.filter(Boolean)
.join(" ");
return (
<label
className={[
"group flex select-none items-start gap-3",
disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer",
].join(" ")}
>
<span className="relative mt-0.5 inline-flex">
<input
ref={inputRef}
type="checkbox"
className="peer sr-only"
checked={checked}
disabled={disabled}
aria-describedby={descId}
onChange={(event) => onChange?.(event.target.checked)}
/>
<span aria-hidden="true" className={boxClasses}>
{isIndeterminate ? (
<motion.svg
viewBox="0 0 16 16"
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
>
<motion.path
d="M4 8 H12"
initial={reduce ? false : { pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 0.2, ease: "easeOut" }}
/>
</motion.svg>
) : active ? (
<motion.svg
viewBox="0 0 16 16"
className="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<motion.path
d="M3.5 8.5 L6.5 11.5 L12.5 4.5"
initial={reduce ? false : { pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.25, ease: "easeOut" }}
/>
</motion.svg>
) : null}
</span>
</span>
<span className="flex min-w-0 flex-col">
<span className="text-sm font-medium leading-5 text-slate-900 dark:text-slate-100">
{label}
</span>
{description ? (
<span
id={descId}
className="mt-0.5 text-[13px] leading-5 text-slate-500 dark:text-slate-400"
>
{description}
</span>
) : null}
</span>
</label>
);
}
type PrefKey = "product" | "security" | "billing";
const NOTIFY_ITEMS: { key: PrefKey; label: string; description: string }[] = [
{
key: "product",
label: "Product updates",
description: "New features, improvements, and roadmap notes.",
},
{
key: "security",
label: "Security alerts",
description: "Sign-in attempts and password changes.",
},
{
key: "billing",
label: "Billing & invoices",
description: "Receipts, renewals, and payment failures.",
},
];
function Card({
title,
eyebrow,
live,
children,
}: {
title: string;
eyebrow: string;
live?: boolean;
children: ReactNode;
}) {
return (
<div className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="mb-5 flex items-center gap-2">
{live ? (
<span className="tglcbx-dot h-2 w-2 rounded-full bg-emerald-500" />
) : (
<span className="h-2 w-2 rounded-full bg-slate-300 dark:bg-slate-600" />
)}
<span className="text-[11px] font-semibold uppercase tracking-[0.14em] text-indigo-600 dark:text-indigo-400">
{eyebrow}
</span>
</div>
<h3 className="mb-4 text-base font-semibold text-slate-900 dark:text-slate-100">
{title}
</h3>
{children}
</div>
);
}
export default function TglCheckboxSet() {
const [prefs, setPrefs] = useState<Record<PrefKey, boolean>>({
product: true,
security: true,
billing: false,
});
const values = Object.values(prefs);
const allChecked = values.every(Boolean);
const noneChecked = values.every((value) => !value);
const someChecked = !allChecked && !noneChecked;
const selectedCount = values.filter(Boolean).length;
const setPref = (key: PrefKey, value: boolean) =>
setPrefs((prev) => ({ ...prev, [key]: value }));
const toggleAll = () => {
const next = !allChecked;
setPrefs({ product: next, security: next, billing: next });
};
const [refDefault, setRefDefault] = useState(false);
const [refChecked, setRefChecked] = useState(true);
const [refMixed, setRefMixed] = useState(false);
const [agreed, setAgreed] = useState(false);
const [submitted, setSubmitted] = useState(false);
return (
<section className="relative w-full bg-slate-50 px-6 py-16 sm:py-20 dark:bg-slate-950">
<style>{`
@keyframes tglcbx-pulse {
0%, 100% { opacity: 0.45; transform: scale(1); }
50% { opacity: 1; transform: scale(1.4); }
}
.tglcbx-dot { animation: tglcbx-pulse 2.4s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.tglcbx-dot { animation: none !important; }
}
`}</style>
<div className="mx-auto w-full max-w-4xl">
<header className="mb-10 max-w-2xl">
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-indigo-600 dark:text-indigo-400">
Form controls
</p>
<h2 className="mt-2 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-slate-50">
Checkbox set
</h2>
<p className="mt-3 text-sm leading-6 text-slate-600 dark:text-slate-400">
Accessible checkboxes with default, checked, indeterminate, and
disabled states. Keyboard operable, screen-reader friendly, and
tuned for light and dark themes.
</p>
</header>
<div className="grid gap-6 md:grid-cols-2">
<Card eyebrow="Indeterminate" title="Email notifications" live>
<fieldset className="m-0 border-0 p-0">
<legend className="sr-only">Email notification preferences</legend>
<Checkbox
label="All notifications"
checked={allChecked}
indeterminate={someChecked}
onChange={toggleAll}
/>
<div className="mt-4 space-y-4 border-l border-slate-200 pl-4 dark:border-slate-800">
{NOTIFY_ITEMS.map((item) => (
<Checkbox
key={item.key}
label={item.label}
description={item.description}
checked={prefs[item.key]}
onChange={(value) => setPref(item.key, value)}
/>
))}
</div>
</fieldset>
<p
aria-live="polite"
className="mt-5 text-[13px] text-slate-500 dark:text-slate-400"
>
{selectedCount} of {NOTIFY_ITEMS.length} channels enabled
</p>
</Card>
<Card eyebrow="Reference" title="All states">
<div className="space-y-4">
<Checkbox
label="Default (unchecked)"
description="Toggle me on and off."
checked={refDefault}
onChange={setRefDefault}
/>
<Checkbox
label="Checked"
description="Starts selected."
checked={refChecked}
onChange={setRefChecked}
/>
<Checkbox
label="Indeterminate"
description="Click to resolve to checked."
checked={refMixed}
indeterminate={!refMixed}
onChange={setRefMixed}
/>
<Checkbox
label="Disabled (unchecked)"
description="Not interactive."
checked={false}
disabled
/>
<Checkbox
label="Disabled (checked)"
description="Not interactive."
checked
disabled
/>
</div>
</Card>
</div>
<div className="mt-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<div className="flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
<Checkbox
label="I agree to the Terms of Service"
description="You can review the full terms before continuing."
checked={agreed}
onChange={(value) => {
setAgreed(value);
setSubmitted(false);
}}
/>
<button
type="button"
disabled={!agreed}
onClick={() => setSubmitted(true)}
className={[
"inline-flex shrink-0 items-center justify-center rounded-xl px-5 py-2.5 text-sm font-semibold",
"transition-colors duration-200",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 focus-visible:ring-offset-2",
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
agreed
? "bg-indigo-600 text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
: "cursor-not-allowed bg-slate-100 text-slate-400 dark:bg-slate-800 dark:text-slate-500",
].join(" ")}
>
Create account
</button>
</div>
<p
aria-live="polite"
className="mt-4 text-[13px] text-emerald-600 dark:text-emerald-400"
>
{submitted ? "Account created — welcome aboard." : " "}
</p>
</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 Custom Toggle
Originalcustom animated tick checkboxes

Checkbox Card Toggle
Originalselectable card checkboxes

Checkbox Indeterminate Toggle
Originalparent/child indeterminate checkboxes

Checkbox List Toggle
Originalcheckbox list with select-all

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

