25 / 29
All 29 in 29 Tailwind CSS Checkboxes
- 01Tailwind CSS Toggle Switch — iOS-Style SaaS Settings
- 02Tailwind Filter Chip Checkbox — Style Checkbox as Button
- 03Tailwind Pricing Plan Card Select — Checkbox-Style Selectable Cards
- 04Pure Tailwind CSS Accordion with Checkbox — No JavaScript
- 05Tailwind Indeterminate Checkbox — Select All with Tri-State
- 06Accessible Custom Checkbox in Tailwind CSS — WCAG Focus Ring
- 07Tailwind Slide Toggle with ON/OFF Labels — GPU translate3d
- 08Neon Glow Checkbox in Tailwind CSS — Dark Mode Gaming UI
- 09Animated SVG Checkmark in Tailwind CSS — stroke-dashoffset Draw
- 10Bouncy Checkbox Animation in Tailwind CSS — Spring cubic-bezier
- 11Glassmorphism Checkbox in Tailwind CSS — backdrop-blur Frosted Glass
- 12Neumorphism Checkbox in Tailwind CSS — Soft UI
- 13Liquid Fill Checkbox Animation in Tailwind CSS — Water Wave
- 14Rotating Ring Checkbox in Tailwind CSS — Spin-to-Check
- 15Brutalist Checkbox in Tailwind CSS — Neo-Brutalism Hard Shadow
- 16Animated Gradient Checkbox in Tailwind CSS — Rainbow Sweep
- 173D Flip Checkbox in Tailwind CSS — perspective & backface-hidden
- 18Material Ripple Checkbox in Tailwind CSS — State Layer & Wave
- 19Retro Pixel Art Checkbox in Tailwind CSS — 8-Bit Arcade Menu
- 20Tailwind CSS To-Do List Checkbox with Animated Strikethrough
- 21Gooey Blob Checkbox in Tailwind CSS — blur + contrast Filter
- 22Tailwind Dark Mode Toggle Switch — Sun & Moon, No JavaScript
- 23Pulse Ring Checkbox in Tailwind CSS — Live Status Indicator
- 24Tailwind CSS Dash-to-Check SVG Morph Checkbox
- 25Tri-State Checkbox in Tailwind CSS + JavaScript — Done / N/A / Unset
- 26Magnetic Hover Checkbox in Tailwind CSS + JavaScript
- 27Confetti Checkbox Animation in Tailwind CSS — Pure CSS Particle Burst
- 28Slide-In Label Checkbox in Tailwind CSS — Saved Tag Reveal
- 29Aurora Gradient Toggle Switch in Tailwind CSS — Premium Dark UI
Tri-State Checkbox in Tailwind CSS + JavaScript — Done / N/A / Unset
A review checklist whose boxes cycle Unset → Done → N/A. The script only writes aria-checked; every visual — fill colour, glyph, status chip — is an ARIA variant: group-aria-checked/btn: and group-aria-[checked=mixed]/btn:. Now the whole row is clickable and every target is 44px.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tcb-25 w-full min-h-svh grid place-items-center bg-tcb25-bg px-5 py-14 font-tcb25-sans text-tcb25-ink selection:bg-tcb25-ring selection:text-white" data-tcb25 aria-label="Multi-state review checklist">
<div class="w-full max-w-[27rem] divide-y divide-tcb25-line rounded-2xl border border-tcb25-line bg-white px-3 py-1">
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="true" aria-labelledby="tcb25-a11y" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-a11y" class="flex-1 text-[15px]">Accessibility audit</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="mixed" aria-labelledby="tcb25-perf" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-perf" class="flex-1 text-[15px]">Performance budget</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="false" aria-labelledby="tcb25-legal" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-legal" class="flex-1 text-[15px]">Legal review</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
</div>
</section><section class="tcb-25 w-full min-h-svh grid place-items-center bg-tcb25-bg px-5 py-14 font-tcb25-sans text-tcb25-ink selection:bg-tcb25-ring selection:text-white" data-tcb25 aria-label="Multi-state review checklist">
<div class="w-full max-w-[27rem] divide-y divide-tcb25-line rounded-2xl border border-tcb25-line bg-white px-3 py-1">
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="true" aria-labelledby="tcb25-a11y" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-a11y" class="flex-1 text-[15px]">Accessibility audit</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="mixed" aria-labelledby="tcb25-perf" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-perf" class="flex-1 text-[15px]">Performance budget</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="false" aria-labelledby="tcb25-legal" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-legal" class="flex-1 text-[15px]">Legal review</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
</div>
</section>@import "tailwindcss";
@theme {
--color-tcb25-bg: #f4f5f7;
--color-tcb25-bg: oklch(0.97 0.004 260);
--color-tcb25-ink: #2a2f3a;
--color-tcb25-ink: oklch(0.3 0.02 265);
--color-tcb25-mut: #5d6472;
--color-tcb25-mut: oklch(0.49 0.02 265);
--color-tcb25-line: #eef0f3;
--color-tcb25-line: oklch(0.953 0.005 265);
--color-tcb25-chip: #f1f2f5;
--color-tcb25-chip: oklch(0.96 0.005 265);
--color-tcb25-box: #9aa1ad;
--color-tcb25-box: oklch(0.71 0.02 265);
--color-tcb25-yes: #1f8a4c;
--color-tcb25-yes: oklch(0.55 0.14 152);
--color-tcb25-yesink: #146b39;
--color-tcb25-yesink: oklch(0.46 0.12 152);
--color-tcb25-na: #d4952b;
--color-tcb25-na: oklch(0.7 0.14 70);
--color-tcb25-naink: #8a5a0c;
--color-tcb25-naink: oklch(0.5 0.11 65);
--color-tcb25-ring: #2f7de1;
--color-tcb25-ring: oklch(0.6 0.17 255);
--font-tcb25-sans: 'Geist', system-ui, sans-serif;
--font-tcb25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-25 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tcb25-bg: #f4f5f7;
--color-tcb25-bg: oklch(0.97 0.004 260);
--color-tcb25-ink: #2a2f3a;
--color-tcb25-ink: oklch(0.3 0.02 265);
--color-tcb25-mut: #5d6472;
--color-tcb25-mut: oklch(0.49 0.02 265);
--color-tcb25-line: #eef0f3;
--color-tcb25-line: oklch(0.953 0.005 265);
--color-tcb25-chip: #f1f2f5;
--color-tcb25-chip: oklch(0.96 0.005 265);
--color-tcb25-box: #9aa1ad;
--color-tcb25-box: oklch(0.71 0.02 265);
--color-tcb25-yes: #1f8a4c;
--color-tcb25-yes: oklch(0.55 0.14 152);
--color-tcb25-yesink: #146b39;
--color-tcb25-yesink: oklch(0.46 0.12 152);
--color-tcb25-na: #d4952b;
--color-tcb25-na: oklch(0.7 0.14 70);
--color-tcb25-naink: #8a5a0c;
--color-tcb25-naink: oklch(0.5 0.11 65);
--color-tcb25-ring: #2f7de1;
--color-tcb25-ring: oklch(0.6 0.17 255);
--font-tcb25-sans: 'Geist', system-ui, sans-serif;
--font-tcb25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-25 {
width: 100%;
min-height: 100svh;
}(() => {
const CYCLE = ['false', 'true', 'mixed'];
document.querySelectorAll('[data-tcb25]').forEach((root) => {
if (root.dataset.wired) return;
root.dataset.wired = '1';
root.querySelectorAll('[data-row]').forEach((row) => {
const btn = row.querySelector('[data-cycle]');
btn.addEventListener('click', () => {
const cur = btn.getAttribute('aria-checked') || 'false';
btn.setAttribute('aria-checked', CYCLE[(CYCLE.indexOf(cur) + 1) % CYCLE.length]);
});
row.addEventListener('click', (e) => { if (!e.target.closest('[data-cycle]')) btn.click(); });
});
});
})();(() => {
const CYCLE = ['false', 'true', 'mixed'];
document.querySelectorAll('[data-tcb25]').forEach((root) => {
if (root.dataset.wired) return;
root.dataset.wired = '1';
root.querySelectorAll('[data-row]').forEach((row) => {
const btn = row.querySelector('[data-cycle]');
btn.addEventListener('click', () => {
const cur = btn.getAttribute('aria-checked') || 'false';
btn.setAttribute('aria-checked', CYCLE[(CYCLE.indexOf(cur) + 1) % CYCLE.length]);
});
row.addEventListener('click', (e) => { if (!e.target.closest('[data-cycle]')) btn.click(); });
});
});
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tcb25-bg: #f4f5f7;
--color-tcb25-bg: oklch(0.97 0.004 260);
--color-tcb25-ink: #2a2f3a;
--color-tcb25-ink: oklch(0.3 0.02 265);
--color-tcb25-mut: #5d6472;
--color-tcb25-mut: oklch(0.49 0.02 265);
--color-tcb25-line: #eef0f3;
--color-tcb25-line: oklch(0.953 0.005 265);
--color-tcb25-chip: #f1f2f5;
--color-tcb25-chip: oklch(0.96 0.005 265);
--color-tcb25-box: #9aa1ad;
--color-tcb25-box: oklch(0.71 0.02 265);
--color-tcb25-yes: #1f8a4c;
--color-tcb25-yes: oklch(0.55 0.14 152);
--color-tcb25-yesink: #146b39;
--color-tcb25-yesink: oklch(0.46 0.12 152);
--color-tcb25-na: #d4952b;
--color-tcb25-na: oklch(0.7 0.14 70);
--color-tcb25-naink: #8a5a0c;
--color-tcb25-naink: oklch(0.5 0.11 65);
--color-tcb25-ring: #2f7de1;
--color-tcb25-ring: oklch(0.6 0.17 255);
--font-tcb25-sans: 'Geist', system-ui, sans-serif;
--font-tcb25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-25 {
width: 100%;
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tcb25-bg: #f4f5f7;
--color-tcb25-bg: oklch(0.97 0.004 260);
--color-tcb25-ink: #2a2f3a;
--color-tcb25-ink: oklch(0.3 0.02 265);
--color-tcb25-mut: #5d6472;
--color-tcb25-mut: oklch(0.49 0.02 265);
--color-tcb25-line: #eef0f3;
--color-tcb25-line: oklch(0.953 0.005 265);
--color-tcb25-chip: #f1f2f5;
--color-tcb25-chip: oklch(0.96 0.005 265);
--color-tcb25-box: #9aa1ad;
--color-tcb25-box: oklch(0.71 0.02 265);
--color-tcb25-yes: #1f8a4c;
--color-tcb25-yes: oklch(0.55 0.14 152);
--color-tcb25-yesink: #146b39;
--color-tcb25-yesink: oklch(0.46 0.12 152);
--color-tcb25-na: #d4952b;
--color-tcb25-na: oklch(0.7 0.14 70);
--color-tcb25-naink: #8a5a0c;
--color-tcb25-naink: oklch(0.5 0.11 65);
--color-tcb25-ring: #2f7de1;
--color-tcb25-ring: oklch(0.6 0.17 255);
--font-tcb25-sans: 'Geist', system-ui, sans-serif;
--font-tcb25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-25 {
width: 100%;
min-height: 100svh;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
.cb-25__btn[aria-checked="mixed"] .cb-25__boxbecamegroup-aria-[checked=mixed]/btn:; the status::afterrules becamepeer-aria-*:after:content-[…].- The two mask glyphs became two stacked SVGs cross-fading on ARIA variants.
- UI upgrade from the CSS original: the button is a 44×44 target (was 24×24), the label is its accessible name via
aria-labelledby, and clicking anywhere on the row cycles the state. - UI upgrade: the status is a tinted chip with dark text (Done 6.4:1, N/A 6.1:1) — the original printed amber text on white at 2.3:1.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tri-State Checkbox in Tailwind CSS + JavaScript — Done / N/A / Unset
Source: https://codefronts.com/components/tailwind-css-checkboxes/tri-state-checkbox-tailwind-javascript/
A review checklist whose boxes cycle Unset → Done → N/A. The script only writes aria-checked; every visual — fill colour, glyph, status chip — is an ARIA variant: group-aria-checked/btn: and group-aria-[checked=mixed]/btn:. Now the whole row is clickable and every target is 44px.
## HTML
```html
<section class="tcb-25 w-full min-h-svh grid place-items-center bg-tcb25-bg px-5 py-14 font-tcb25-sans text-tcb25-ink selection:bg-tcb25-ring selection:text-white" data-tcb25 aria-label="Multi-state review checklist">
<div class="w-full max-w-[27rem] divide-y divide-tcb25-line rounded-2xl border border-tcb25-line bg-white px-3 py-1">
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="true" aria-labelledby="tcb25-a11y" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-a11y" class="flex-1 text-[15px]">Accessibility audit</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="mixed" aria-labelledby="tcb25-perf" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-perf" class="flex-1 text-[15px]">Performance budget</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="false" aria-labelledby="tcb25-legal" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-legal" class="flex-1 text-[15px]">Legal review</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcb25-bg: #f4f5f7;
--color-tcb25-bg: oklch(0.97 0.004 260);
--color-tcb25-ink: #2a2f3a;
--color-tcb25-ink: oklch(0.3 0.02 265);
--color-tcb25-mut: #5d6472;
--color-tcb25-mut: oklch(0.49 0.02 265);
--color-tcb25-line: #eef0f3;
--color-tcb25-line: oklch(0.953 0.005 265);
--color-tcb25-chip: #f1f2f5;
--color-tcb25-chip: oklch(0.96 0.005 265);
--color-tcb25-box: #9aa1ad;
--color-tcb25-box: oklch(0.71 0.02 265);
--color-tcb25-yes: #1f8a4c;
--color-tcb25-yes: oklch(0.55 0.14 152);
--color-tcb25-yesink: #146b39;
--color-tcb25-yesink: oklch(0.46 0.12 152);
--color-tcb25-na: #d4952b;
--color-tcb25-na: oklch(0.7 0.14 70);
--color-tcb25-naink: #8a5a0c;
--color-tcb25-naink: oklch(0.5 0.11 65);
--color-tcb25-ring: #2f7de1;
--color-tcb25-ring: oklch(0.6 0.17 255);
--font-tcb25-sans: 'Geist', system-ui, sans-serif;
--font-tcb25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-25 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const CYCLE = ['false', 'true', 'mixed'];
document.querySelectorAll('[data-tcb25]').forEach((root) => {
if (root.dataset.wired) return;
root.dataset.wired = '1';
root.querySelectorAll('[data-row]').forEach((row) => {
const btn = row.querySelector('[data-cycle]');
btn.addEventListener('click', () => {
const cur = btn.getAttribute('aria-checked') || 'false';
btn.setAttribute('aria-checked', CYCLE[(CYCLE.indexOf(cur) + 1) % CYCLE.length]);
});
row.addEventListener('click', (e) => { if (!e.target.closest('[data-cycle]')) btn.click(); });
});
});
})();
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tri-State Checkbox in Tailwind CSS + JavaScript — Done / N/A / Unset
Source: https://codefronts.com/components/tailwind-css-checkboxes/tri-state-checkbox-tailwind-javascript/
A review checklist whose boxes cycle Unset → Done → N/A. The script only writes aria-checked; every visual — fill colour, glyph, status chip — is an ARIA variant: group-aria-checked/btn: and group-aria-[checked=mixed]/btn:. Now the whole row is clickable and every target is 44px.
## HTML
```html
<section class="tcb-25 w-full min-h-svh grid place-items-center bg-tcb25-bg px-5 py-14 font-tcb25-sans text-tcb25-ink selection:bg-tcb25-ring selection:text-white" data-tcb25 aria-label="Multi-state review checklist">
<div class="w-full max-w-[27rem] divide-y divide-tcb25-line rounded-2xl border border-tcb25-line bg-white px-3 py-1">
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="true" aria-labelledby="tcb25-a11y" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-a11y" class="flex-1 text-[15px]">Accessibility audit</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="mixed" aria-labelledby="tcb25-perf" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-perf" class="flex-1 text-[15px]">Performance budget</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
<div class="flex min-h-15 cursor-pointer items-center gap-2 pr-2" data-row><button type="button" role="checkbox" aria-checked="false" aria-labelledby="tcb25-legal" data-cycle class="group/btn peer grid size-11 flex-none cursor-pointer place-items-center rounded-lg focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tcb25-ring"><span aria-hidden="true" class="grid size-6 place-items-center rounded-[7px] border-2 border-tcb25-box bg-white text-white transition-colors duration-200 group-aria-checked/btn:border-tcb25-yes group-aria-checked/btn:bg-tcb25-yes group-aria-[checked=mixed]/btn:border-tcb25-na group-aria-[checked=mixed]/btn:bg-tcb25-na motion-reduce:transition-none"><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-checked/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" class="size-3.5"><path d="M20 6 9 17l-5-5"/></svg></span><span class="col-start-1 row-start-1 opacity-0 transition-opacity group-aria-[checked=mixed]/btn:opacity-100"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" class="size-3.5"><path d="M6 12h12"/></svg></span></span></button><span id="tcb25-legal" class="flex-1 text-[15px]">Legal review</span><span aria-hidden="true" class="rounded-md bg-tcb25-chip px-2 py-1 font-tcb25-mono text-[11px] font-semibold uppercase tracking-[0.08em] text-tcb25-mut after:content-['Unset'] peer-aria-checked:bg-tcb25-yes/12 peer-aria-checked:text-tcb25-yesink peer-aria-checked:after:content-['Done'] peer-aria-[checked=mixed]:bg-tcb25-na/15 peer-aria-[checked=mixed]:text-tcb25-naink peer-aria-[checked=mixed]:after:content-['N/A']"></span></div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tcb25-bg: #f4f5f7;
--color-tcb25-bg: oklch(0.97 0.004 260);
--color-tcb25-ink: #2a2f3a;
--color-tcb25-ink: oklch(0.3 0.02 265);
--color-tcb25-mut: #5d6472;
--color-tcb25-mut: oklch(0.49 0.02 265);
--color-tcb25-line: #eef0f3;
--color-tcb25-line: oklch(0.953 0.005 265);
--color-tcb25-chip: #f1f2f5;
--color-tcb25-chip: oklch(0.96 0.005 265);
--color-tcb25-box: #9aa1ad;
--color-tcb25-box: oklch(0.71 0.02 265);
--color-tcb25-yes: #1f8a4c;
--color-tcb25-yes: oklch(0.55 0.14 152);
--color-tcb25-yesink: #146b39;
--color-tcb25-yesink: oklch(0.46 0.12 152);
--color-tcb25-na: #d4952b;
--color-tcb25-na: oklch(0.7 0.14 70);
--color-tcb25-naink: #8a5a0c;
--color-tcb25-naink: oklch(0.5 0.11 65);
--color-tcb25-ring: #2f7de1;
--color-tcb25-ring: oklch(0.6 0.17 255);
--font-tcb25-sans: 'Geist', system-ui, sans-serif;
--font-tcb25-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's utilities, so the
stage is sized before the browser build compiles. */
.tcb-25 {
width: 100%;
min-height: 100svh;
}
```
## JavaScript
```js
(() => {
const CYCLE = ['false', 'true', 'mixed'];
document.querySelectorAll('[data-tcb25]').forEach((root) => {
if (root.dataset.wired) return;
root.dataset.wired = '1';
root.querySelectorAll('[data-row]').forEach((row) => {
const btn = row.querySelector('[data-cycle]');
btn.addEventListener('click', () => {
const cur = btn.getAttribute('aria-checked') || 'false';
btn.setAttribute('aria-checked', CYCLE[(CYCLE.indexOf(cur) + 1) % CYCLE.length]);
});
row.addEventListener('click', (e) => { if (!e.target.closest('[data-cycle]')) btn.click(); });
});
});
})();
```How this works
Why a button. Native checkboxes have only two user-reachable states. A <button role="checkbox"> gets Space/Enter handling for free and can legitimately report aria-checked="mixed".
ARIA is the styling API. The script sets one attribute; Tailwind's aria-checked: variant (for true) and the arbitrary aria-[checked=mixed]: handle the rest. No class names in JS.
Named groups (group/btn) keep the box reading the button's state even though the row is also a group.
Make it yours
- Different cycle: edit the
CYCLEarray (e.g. skip N/A). - Colours:
--color-tcb25-yesand--color-tcb25-na. - Persist: serialise each button's
aria-checkedto storage. - Reverse cycle on Shift+click: read
e.shiftKey.
Gotchas — read before shipping
mixedmeans "partially checked" to assistive tech; if N/A means something else, explain it in the label.- Don't put
role="checkbox"on a div — you'd lose native keyboard handling. - The row's click handler ignores clicks on the button itself to avoid double-cycling.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
ARIA attribute selectors are universal.