06 / 25
All 25 in 25 Tailwind CSS FAQ Accordion
- 01Tailwind Peer-Checked FAQ Accordion with No JavaScript
- 02Tailwind Native Details/Summary FAQ with Custom Styled Icons
- 03Tailwind Exclusive Accordion — Only One FAQ Open at a Time
- 04Tailwind Multi-Open FAQ Accordion with a Live CSS Counter
- 05Tailwind CSS FAQ Accordion Template (Free, Copy-Paste)
- 06Tailwind Dark Mode FAQ Accordion with light-dark() and No .dark Class
- 07Tailwind Glassmorphism FAQ Accordion with Frosted Backdrop Blur
- 08Tailwind Minimalist E-Commerce FAQ Accordion for Product Pages
- 09Tailwind Neumorphic Soft-UI FAQ Accordion Cards
- 10Tailwind Animate Height Auto FAQ with grid-rows 0fr to 1fr
- 11Tailwind FAQ Chevron Rotate 180 Degrees on Toggle
- 12Tailwind Animated Plus to Minus Icon Morph FAQ Accordion
- 13Tailwind Spring Bounce FAQ Accordion with linear() Easing
- 14Tailwind Two-Column FAQ Accordion Grid with Independent Columns
- 15Tailwind Horizontal FAQ Accordion with Sideways Expanding Panels
- 16Tailwind Categorised FAQ Help Centre with a Sticky Sidebar Menu
- 17Tailwind Card-Style FAQ Panels with a Layered Shadow Elevation System
- 18Tailwind FAQ Accordion with a Live Search Bar Filter
- 19Tailwind Expand All and Collapse All FAQ Accordion Buttons
- 20Tailwind FAQ with a Still Have Questions Contact CTA Card
- 21Tailwind Accessible ARIA FAQ Accordion with Keyboard Navigation
- 22Tailwind FAQ with FAQPage JSON-LD Schema for Rich Snippets and AI Search
- 23Bootstrap 5 Accordion Rebuilt in Tailwind (and the JavaScript You Delete)
- 24Tailwind FAQ Component in React, Vue, Svelte and Astro (One Port Recipe)
- 25Tailwind FAQ with Voice Search Using the Speech Recognition API
Tailwind Dark Mode FAQ Accordion with light-dark() and No .dark Class
The counter-argument to dark:. Every colour is declared once as a light-dark() pair inside @theme, and a single color-scheme flip re-skins the whole component — including native scrollbars, form controls and selection colours that a .dark class never reaches. A true two-way switch that starts from the visitor's OS preference, with zero duplicated utilities, a small seed script, and a View Transition cross-fade.
Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49
Cruip · Affiliate link
The code
<section class="tfa-06 group/tfa06 w-full min-h-svh grid place-items-center bg-tfa06-bg font-tfa06-sans text-tfa06-ink px-5 py-12 [color-scheme:light] has-[#tfa06-mode:checked]:[color-scheme:dark]">
<div class="w-[min(660px,100%)]">
<div class="flex items-center justify-between gap-4">
<span class="text-[14px] font-extrabold tracking-[-0.01em]">Nightshift Docs</span>
<input id="tfa06-mode" type="checkbox" role="switch" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa06-mode" title="Dark theme" class="flex min-h-11 cursor-pointer items-center gap-2 rounded-full border border-tfa06-edge bg-tfa06-panel px-2.5 py-1.5 text-tfa06-mut hover:border-tfa06-accedge peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-tfa06-acc">
<span aria-hidden="true" class="text-tfa06-sun"><svg viewBox="0 0 24 24" width="13" height="13"><circle cx="12" cy="12" r="4.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5 5l2.1 2.1M16.9 16.9L19 19M19 5l-2.1 2.1M7.1 16.9L5 19"/></g></svg></span>
<span aria-hidden="true" class="relative h-5 w-9 rounded-full bg-tfa06-edge group-has-[#tfa06-mode:checked]/tfa06:bg-tfa06-acc after:absolute after:left-0.5 after:top-0.5 after:size-4 after:rounded-full after:bg-tfa06-panel after:shadow-[0_1px_4px_var(--color-tfa06-shadow)] after:transition-transform after:duration-300 after:ease-[cubic-bezier(0.34,1.56,0.64,1)] group-has-[#tfa06-mode:checked]/tfa06:after:translate-x-4 motion-reduce:after:transition-none"></span>
<span aria-hidden="true" class="group-has-[#tfa06-mode:checked]/tfa06:text-tfa06-acc"><svg viewBox="0 0 24 24" width="13" height="13"><path d="M20 14.5A8.5 8.5 0 019.5 4a8.5 8.5 0 1010.5 10.5z" fill="currentColor"/></svg></span>
<span class="sr-only">Dark theme</span>
</label>
</div>
<header class="mt-7">
<h1 class="text-[clamp(28px,5vw,38px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Frequently asked questions</h1>
<p class="mt-3 max-w-[54ch] text-[14.5px] leading-[1.7] text-tfa06-mut">Every colour below is one <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">light-dark()</code> token. Flip the switch — or your OS theme — and the whole component re-skins.</p>
</header>
<div data-panel class="mt-6 flex flex-col gap-2.5">
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge" open>
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Does dark mode save battery?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">On OLED screens, meaningfully — black pixels are unlit. On LCD panels the backlight runs regardless, so the gain is comfort, not power.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Why not just add a .dark class?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Class-swapping duplicates every colour rule and misses native UI: scrollbars, form controls and selection colours stay light. <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">color-scheme</code> re-skins those for free, and <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">light-dark()</code> keeps each token in one place.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Should dark mode be pure black?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Usually no — near-black surfaces (this panel is oklch 0.22) reduce smearing on OLED and let shadows still read. Reserve true black for media canvases.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">How do I persist the user's choice?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Store one value ('light' / 'dark' / 'auto') in localStorage and restore it before first paint in a tiny inline script. The utilities need zero changes — you are only setting the checkbox.</p>
</details>
</div>
<p class="mt-6 text-[12.5px] text-tfa06-mut">Starts from your OS preference, then the switch wins both ways. Checked = <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">has-[#tfa06-mode:checked]:[color-scheme:dark]</code>.</p>
</div>
</section><section class="tfa-06 group/tfa06 w-full min-h-svh grid place-items-center bg-tfa06-bg font-tfa06-sans text-tfa06-ink px-5 py-12 [color-scheme:light] has-[#tfa06-mode:checked]:[color-scheme:dark]">
<div class="w-[min(660px,100%)]">
<div class="flex items-center justify-between gap-4">
<span class="text-[14px] font-extrabold tracking-[-0.01em]">Nightshift Docs</span>
<input id="tfa06-mode" type="checkbox" role="switch" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa06-mode" title="Dark theme" class="flex min-h-11 cursor-pointer items-center gap-2 rounded-full border border-tfa06-edge bg-tfa06-panel px-2.5 py-1.5 text-tfa06-mut hover:border-tfa06-accedge peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-tfa06-acc">
<span aria-hidden="true" class="text-tfa06-sun"><svg viewBox="0 0 24 24" width="13" height="13"><circle cx="12" cy="12" r="4.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5 5l2.1 2.1M16.9 16.9L19 19M19 5l-2.1 2.1M7.1 16.9L5 19"/></g></svg></span>
<span aria-hidden="true" class="relative h-5 w-9 rounded-full bg-tfa06-edge group-has-[#tfa06-mode:checked]/tfa06:bg-tfa06-acc after:absolute after:left-0.5 after:top-0.5 after:size-4 after:rounded-full after:bg-tfa06-panel after:shadow-[0_1px_4px_var(--color-tfa06-shadow)] after:transition-transform after:duration-300 after:ease-[cubic-bezier(0.34,1.56,0.64,1)] group-has-[#tfa06-mode:checked]/tfa06:after:translate-x-4 motion-reduce:after:transition-none"></span>
<span aria-hidden="true" class="group-has-[#tfa06-mode:checked]/tfa06:text-tfa06-acc"><svg viewBox="0 0 24 24" width="13" height="13"><path d="M20 14.5A8.5 8.5 0 019.5 4a8.5 8.5 0 1010.5 10.5z" fill="currentColor"/></svg></span>
<span class="sr-only">Dark theme</span>
</label>
</div>
<header class="mt-7">
<h1 class="text-[clamp(28px,5vw,38px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Frequently asked questions</h1>
<p class="mt-3 max-w-[54ch] text-[14.5px] leading-[1.7] text-tfa06-mut">Every colour below is one <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">light-dark()</code> token. Flip the switch — or your OS theme — and the whole component re-skins.</p>
</header>
<div data-panel class="mt-6 flex flex-col gap-2.5">
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge" open>
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Does dark mode save battery?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">On OLED screens, meaningfully — black pixels are unlit. On LCD panels the backlight runs regardless, so the gain is comfort, not power.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Why not just add a .dark class?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Class-swapping duplicates every colour rule and misses native UI: scrollbars, form controls and selection colours stay light. <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">color-scheme</code> re-skins those for free, and <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">light-dark()</code> keeps each token in one place.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Should dark mode be pure black?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Usually no — near-black surfaces (this panel is oklch 0.22) reduce smearing on OLED and let shadows still read. Reserve true black for media canvases.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">How do I persist the user's choice?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Store one value ('light' / 'dark' / 'auto') in localStorage and restore it before first paint in a tiny inline script. The utilities need zero changes — you are only setting the checkbox.</p>
</details>
</div>
<p class="mt-6 text-[12.5px] text-tfa06-mut">Starts from your OS preference, then the switch wins both ways. Checked = <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">has-[#tfa06-mode:checked]:[color-scheme:dark]</code>.</p>
</div>
</section>@import "tailwindcss";
@theme {
/* Every entry: a plain light value first (pre-2024 floor), then the light-dark() pair. */
--color-tfa06-bg: #f8f9fc;
--color-tfa06-bg: light-dark(oklch(0.975 0.008 250), oklch(0.17 0.02 258));
--color-tfa06-panel: #ffffff;
--color-tfa06-panel: light-dark(oklch(1 0 0), oklch(0.22 0.022 258));
--color-tfa06-ink: #2b2f3a;
--color-tfa06-ink: light-dark(oklch(0.22 0.025 258), oklch(0.96 0.006 250));
--color-tfa06-mut: #5f6472;
--color-tfa06-mut: light-dark(oklch(0.47 0.02 255), oklch(0.79 0.015 252));
--color-tfa06-edge: #e4e6ee;
--color-tfa06-edge: light-dark(oklch(0.91 0.01 250), oklch(0.32 0.025 256));
--color-tfa06-accedge: #8ba6e8;
--color-tfa06-accedge: light-dark(oklch(0.72 0.1 255), oklch(0.55 0.11 250));
--color-tfa06-tint: #eaeffc;
--color-tfa06-tint: light-dark(oklch(0.95 0.025 255), oklch(0.27 0.045 252));
--color-tfa06-acc: #3f5fd0;
--color-tfa06-acc: light-dark(oklch(0.48 0.19 260), oklch(0.8 0.12 250));
--color-tfa06-sun: #d99b2b;
--color-tfa06-sun: light-dark(oklch(0.68 0.14 75), oklch(0.78 0.14 75));
--color-tfa06-shadow: #d7dae6;
--color-tfa06-shadow: light-dark(oklch(0.3 0.05 255 / 0.1), oklch(0 0 0 / 0.4));
--font-tfa06-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Scheme default is the [color-scheme:light] utility on the root, NOT a rule here:
unlayered CSS beats Tailwind's utilities layer, so declaring it here would silently defeat
has-checked:[color-scheme:dark]. */
.tfa-06 {
width: 100%;
min-height: 100svh;
}
/* light-dark() tokens do not change computed value when color-scheme flips, so a CSS colour
transition on them freezes at the start colour in Chromium. The cross-fade is a View
Transition instead; these two lines just set its timing. */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.4,0,.2,1);
}
.tfa-06 [data-panel] {
interpolate-size: allow-keywords;
}
@supports (interpolate-size: allow-keywords) {
.tfa-06 details::details-content {
height: 0;
overflow: clip;
opacity: 0;
transition: height .38s cubic-bezier(.4,0,.2,1), opacity .3s, content-visibility .38s allow-discrete;
}
.tfa-06 details[open]::details-content {
height: auto;
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.tfa-06 details::details-content {
transition-duration: 1ms;
}
}@import "tailwindcss";
@theme {
/* Every entry: a plain light value first (pre-2024 floor), then the light-dark() pair. */
--color-tfa06-bg: #f8f9fc;
--color-tfa06-bg: light-dark(oklch(0.975 0.008 250), oklch(0.17 0.02 258));
--color-tfa06-panel: #ffffff;
--color-tfa06-panel: light-dark(oklch(1 0 0), oklch(0.22 0.022 258));
--color-tfa06-ink: #2b2f3a;
--color-tfa06-ink: light-dark(oklch(0.22 0.025 258), oklch(0.96 0.006 250));
--color-tfa06-mut: #5f6472;
--color-tfa06-mut: light-dark(oklch(0.47 0.02 255), oklch(0.79 0.015 252));
--color-tfa06-edge: #e4e6ee;
--color-tfa06-edge: light-dark(oklch(0.91 0.01 250), oklch(0.32 0.025 256));
--color-tfa06-accedge: #8ba6e8;
--color-tfa06-accedge: light-dark(oklch(0.72 0.1 255), oklch(0.55 0.11 250));
--color-tfa06-tint: #eaeffc;
--color-tfa06-tint: light-dark(oklch(0.95 0.025 255), oklch(0.27 0.045 252));
--color-tfa06-acc: #3f5fd0;
--color-tfa06-acc: light-dark(oklch(0.48 0.19 260), oklch(0.8 0.12 250));
--color-tfa06-sun: #d99b2b;
--color-tfa06-sun: light-dark(oklch(0.68 0.14 75), oklch(0.78 0.14 75));
--color-tfa06-shadow: #d7dae6;
--color-tfa06-shadow: light-dark(oklch(0.3 0.05 255 / 0.1), oklch(0 0 0 / 0.4));
--font-tfa06-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Scheme default is the [color-scheme:light] utility on the root, NOT a rule here:
unlayered CSS beats Tailwind's utilities layer, so declaring it here would silently defeat
has-checked:[color-scheme:dark]. */
.tfa-06 {
width: 100%;
min-height: 100svh;
}
/* light-dark() tokens do not change computed value when color-scheme flips, so a CSS colour
transition on them freezes at the start colour in Chromium. The cross-fade is a View
Transition instead; these two lines just set its timing. */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.4,0,.2,1);
}
.tfa-06 [data-panel] {
interpolate-size: allow-keywords;
}
@supports (interpolate-size: allow-keywords) {
.tfa-06 details::details-content {
height: 0;
overflow: clip;
opacity: 0;
transition: height .38s cubic-bezier(.4,0,.2,1), opacity .3s, content-visibility .38s allow-discrete;
}
.tfa-06 details[open]::details-content {
height: auto;
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.tfa-06 details::details-content {
transition-duration: 1ms;
}
}(function(){
var root=document.querySelector('.tfa-06');if(!root||root.dataset.tfa06)return;root.dataset.tfa06='1';
var cb=root.querySelector('#tfa06-mode'),mq=window.matchMedia('(prefers-color-scheme: dark)'),touched=false;
cb.checked=mq.matches;
var calm=window.matchMedia('(prefers-reduced-motion: reduce)');
cb.addEventListener('click',function(e){
touched=true;
if(!document.startViewTransition||calm.matches)return;
var next=cb.checked;e.preventDefault();
document.startViewTransition(function(){cb.checked=next;});
});
mq.addEventListener('change',function(e){if(!touched)cb.checked=e.matches;});
})();(function(){
var root=document.querySelector('.tfa-06');if(!root||root.dataset.tfa06)return;root.dataset.tfa06='1';
var cb=root.querySelector('#tfa06-mode'),mq=window.matchMedia('(prefers-color-scheme: dark)'),touched=false;
cb.checked=mq.matches;
var calm=window.matchMedia('(prefers-reduced-motion: reduce)');
cb.addEventListener('click',function(e){
touched=true;
if(!document.startViewTransition||calm.matches)return;
var next=cb.checked;e.preventDefault();
document.startViewTransition(function(){cb.checked=next;});
});
mq.addEventListener('change',function(e){if(!touched)cb.checked=e.matches;});
})();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 {
/* Every entry: a plain light value first (pre-2024 floor), then the light-dark() pair. */
--color-tfa06-bg: #f8f9fc;
--color-tfa06-bg: light-dark(oklch(0.975 0.008 250), oklch(0.17 0.02 258));
--color-tfa06-panel: #ffffff;
--color-tfa06-panel: light-dark(oklch(1 0 0), oklch(0.22 0.022 258));
--color-tfa06-ink: #2b2f3a;
--color-tfa06-ink: light-dark(oklch(0.22 0.025 258), oklch(0.96 0.006 250));
--color-tfa06-mut: #5f6472;
--color-tfa06-mut: light-dark(oklch(0.47 0.02 255), oklch(0.79 0.015 252));
--color-tfa06-edge: #e4e6ee;
--color-tfa06-edge: light-dark(oklch(0.91 0.01 250), oklch(0.32 0.025 256));
--color-tfa06-accedge: #8ba6e8;
--color-tfa06-accedge: light-dark(oklch(0.72 0.1 255), oklch(0.55 0.11 250));
--color-tfa06-tint: #eaeffc;
--color-tfa06-tint: light-dark(oklch(0.95 0.025 255), oklch(0.27 0.045 252));
--color-tfa06-acc: #3f5fd0;
--color-tfa06-acc: light-dark(oklch(0.48 0.19 260), oklch(0.8 0.12 250));
--color-tfa06-sun: #d99b2b;
--color-tfa06-sun: light-dark(oklch(0.68 0.14 75), oklch(0.78 0.14 75));
--color-tfa06-shadow: #d7dae6;
--color-tfa06-shadow: light-dark(oklch(0.3 0.05 255 / 0.1), oklch(0 0 0 / 0.4));
--font-tfa06-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Scheme default is the [color-scheme:light] utility on the root, NOT a rule here:
unlayered CSS beats Tailwind's utilities layer, so declaring it here would silently defeat
has-checked:[color-scheme:dark]. */
.tfa-06 {
width: 100%;
min-height: 100svh;
}
/* light-dark() tokens do not change computed value when color-scheme flips, so a CSS colour
transition on them freezes at the start colour in Chromium. The cross-fade is a View
Transition instead; these two lines just set its timing. */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.4,0,.2,1);
}
.tfa-06 [data-panel] {
interpolate-size: allow-keywords;
}
@supports (interpolate-size: allow-keywords) {
.tfa-06 details::details-content {
height: 0;
overflow: clip;
opacity: 0;
transition: height .38s cubic-bezier(.4,0,.2,1), opacity .3s, content-visibility .38s allow-discrete;
}
.tfa-06 details[open]::details-content {
height: auto;
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.tfa-06 details::details-content {
transition-duration: 1ms;
}
}@import "tailwindcss";
@theme {
/* Every entry: a plain light value first (pre-2024 floor), then the light-dark() pair. */
--color-tfa06-bg: #f8f9fc;
--color-tfa06-bg: light-dark(oklch(0.975 0.008 250), oklch(0.17 0.02 258));
--color-tfa06-panel: #ffffff;
--color-tfa06-panel: light-dark(oklch(1 0 0), oklch(0.22 0.022 258));
--color-tfa06-ink: #2b2f3a;
--color-tfa06-ink: light-dark(oklch(0.22 0.025 258), oklch(0.96 0.006 250));
--color-tfa06-mut: #5f6472;
--color-tfa06-mut: light-dark(oklch(0.47 0.02 255), oklch(0.79 0.015 252));
--color-tfa06-edge: #e4e6ee;
--color-tfa06-edge: light-dark(oklch(0.91 0.01 250), oklch(0.32 0.025 256));
--color-tfa06-accedge: #8ba6e8;
--color-tfa06-accedge: light-dark(oklch(0.72 0.1 255), oklch(0.55 0.11 250));
--color-tfa06-tint: #eaeffc;
--color-tfa06-tint: light-dark(oklch(0.95 0.025 255), oklch(0.27 0.045 252));
--color-tfa06-acc: #3f5fd0;
--color-tfa06-acc: light-dark(oklch(0.48 0.19 260), oklch(0.8 0.12 250));
--color-tfa06-sun: #d99b2b;
--color-tfa06-sun: light-dark(oklch(0.68 0.14 75), oklch(0.78 0.14 75));
--color-tfa06-shadow: #d7dae6;
--color-tfa06-shadow: light-dark(oklch(0.3 0.05 255 / 0.1), oklch(0 0 0 / 0.4));
--font-tfa06-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Scheme default is the [color-scheme:light] utility on the root, NOT a rule here:
unlayered CSS beats Tailwind's utilities layer, so declaring it here would silently defeat
has-checked:[color-scheme:dark]. */
.tfa-06 {
width: 100%;
min-height: 100svh;
}
/* light-dark() tokens do not change computed value when color-scheme flips, so a CSS colour
transition on them freezes at the start colour in Chromium. The cross-fade is a View
Transition instead; these two lines just set its timing. */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.4,0,.2,1);
}
.tfa-06 [data-panel] {
interpolate-size: allow-keywords;
}
@supports (interpolate-size: allow-keywords) {
.tfa-06 details::details-content {
height: 0;
overflow: clip;
opacity: 0;
transition: height .38s cubic-bezier(.4,0,.2,1), opacity .3s, content-visibility .38s allow-discrete;
}
.tfa-06 details[open]::details-content {
height: auto;
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.tfa-06 details::details-content {
transition-duration: 1ms;
}
}<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=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&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=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The whole demo hinges on one decision:
light-dark()pairs go inside@theme, not:root. In@themethey still mintbg-*/text-*/border-*utilities, so the markup carries nodark:prefixes at all — the inverse of demo 05's approach, and the honest comparison between the two. .faq:has(#mode:checked){color-scheme:dark}becomeshas-[#tfa06-mode:checked]:[color-scheme:dark]on the root. The defaultlight darkscheme is also a utility, deliberately: an unlayered CSS rule outranks Tailwind's utilities layer and would make the toggle a no-op.- Two-way by design: a checkbox over
light darkcan only ever force dark, which is a no-op for every visitor already on a dark OS. Defaulting tolightand seeding the checkbox frommatchMediakeeps OS-first behaviour while letting the switch move both ways, and achangelistener keeps following the OS until the user takes over. - Switch internals use a named group (
group/tfa06+group-has-[#tfa06-mode:checked]/tfa06:):peer-checked:only styles the peer's following siblings, never their children, so the track, knob and moon inside the label need the root-level relationship. - The transition trap: the first build faded the whole page with
transition-[background-color,color]. Because alight-dark()token's computed value is the same function in both schemes, Chromium never saw a change to animate toward and froze every transitioned surface on its light colour. Colour transitions are gone from every scheme-dependent surface; a View Transition cross-fades the snapshot instead, skipped under reduced motion. - The knob animation is the one place a spring curve earned its keep:
ease-[cubic-bezier(0.34,1.56,0.64,1)]overshoots about 4%, which is what makes a toggle feel switched rather than moved. - UI upgrade from the CSS original: light-mode muted ink moved
oklch(0.5)→0.47and dark-mode0.68→0.79; both schemes now clear 4.5:1, where the dark one previously sat near 3.6:1. - UI upgrade: the sun glyph got its own light-dark token instead of a hardcoded warm yellow that washed out on the light panel, and the switch track now tints on
peer-checked:so the control state does not rest on knob position alone.
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: Tailwind Dark Mode FAQ Accordion with light-dark() and No .dark Class
Source: https://codefronts.com/snippets/tailwind-css-faq-accordion/tailwind-dark-mode-faq-accordion-light-dark/
The counter-argument to dark:. Every colour is declared once as a light-dark() pair inside @theme, and a single color-scheme flip re-skins the whole component — including native scrollbars, form controls and selection colours that a .dark class never reaches. A true two-way switch that starts from the visitor's OS preference, with zero duplicated utilities, a small seed script, and a View Transition cross-fade.
## HTML
```html
<section class="tfa-06 group/tfa06 w-full min-h-svh grid place-items-center bg-tfa06-bg font-tfa06-sans text-tfa06-ink px-5 py-12 [color-scheme:light] has-[#tfa06-mode:checked]:[color-scheme:dark]">
<div class="w-[min(660px,100%)]">
<div class="flex items-center justify-between gap-4">
<span class="text-[14px] font-extrabold tracking-[-0.01em]">Nightshift Docs</span>
<input id="tfa06-mode" type="checkbox" role="switch" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa06-mode" title="Dark theme" class="flex min-h-11 cursor-pointer items-center gap-2 rounded-full border border-tfa06-edge bg-tfa06-panel px-2.5 py-1.5 text-tfa06-mut hover:border-tfa06-accedge peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-tfa06-acc">
<span aria-hidden="true" class="text-tfa06-sun"><svg viewBox="0 0 24 24" width="13" height="13"><circle cx="12" cy="12" r="4.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5 5l2.1 2.1M16.9 16.9L19 19M19 5l-2.1 2.1M7.1 16.9L5 19"/></g></svg></span>
<span aria-hidden="true" class="relative h-5 w-9 rounded-full bg-tfa06-edge group-has-[#tfa06-mode:checked]/tfa06:bg-tfa06-acc after:absolute after:left-0.5 after:top-0.5 after:size-4 after:rounded-full after:bg-tfa06-panel after:shadow-[0_1px_4px_var(--color-tfa06-shadow)] after:transition-transform after:duration-300 after:ease-[cubic-bezier(0.34,1.56,0.64,1)] group-has-[#tfa06-mode:checked]/tfa06:after:translate-x-4 motion-reduce:after:transition-none"></span>
<span aria-hidden="true" class="group-has-[#tfa06-mode:checked]/tfa06:text-tfa06-acc"><svg viewBox="0 0 24 24" width="13" height="13"><path d="M20 14.5A8.5 8.5 0 019.5 4a8.5 8.5 0 1010.5 10.5z" fill="currentColor"/></svg></span>
<span class="sr-only">Dark theme</span>
</label>
</div>
<header class="mt-7">
<h1 class="text-[clamp(28px,5vw,38px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Frequently asked questions</h1>
<p class="mt-3 max-w-[54ch] text-[14.5px] leading-[1.7] text-tfa06-mut">Every colour below is one <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">light-dark()</code> token. Flip the switch — or your OS theme — and the whole component re-skins.</p>
</header>
<div data-panel class="mt-6 flex flex-col gap-2.5">
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge" open>
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Does dark mode save battery?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">On OLED screens, meaningfully — black pixels are unlit. On LCD panels the backlight runs regardless, so the gain is comfort, not power.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Why not just add a .dark class?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Class-swapping duplicates every colour rule and misses native UI: scrollbars, form controls and selection colours stay light. <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">color-scheme</code> re-skins those for free, and <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">light-dark()</code> keeps each token in one place.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Should dark mode be pure black?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Usually no — near-black surfaces (this panel is oklch 0.22) reduce smearing on OLED and let shadows still read. Reserve true black for media canvases.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">How do I persist the user's choice?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Store one value ('light' / 'dark' / 'auto') in localStorage and restore it before first paint in a tiny inline script. The utilities need zero changes — you are only setting the checkbox.</p>
</details>
</div>
<p class="mt-6 text-[12.5px] text-tfa06-mut">Starts from your OS preference, then the switch wins both ways. Checked = <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">has-[#tfa06-mode:checked]:[color-scheme:dark]</code>.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
/* Every entry: a plain light value first (pre-2024 floor), then the light-dark() pair. */
--color-tfa06-bg: #f8f9fc;
--color-tfa06-bg: light-dark(oklch(0.975 0.008 250), oklch(0.17 0.02 258));
--color-tfa06-panel: #ffffff;
--color-tfa06-panel: light-dark(oklch(1 0 0), oklch(0.22 0.022 258));
--color-tfa06-ink: #2b2f3a;
--color-tfa06-ink: light-dark(oklch(0.22 0.025 258), oklch(0.96 0.006 250));
--color-tfa06-mut: #5f6472;
--color-tfa06-mut: light-dark(oklch(0.47 0.02 255), oklch(0.79 0.015 252));
--color-tfa06-edge: #e4e6ee;
--color-tfa06-edge: light-dark(oklch(0.91 0.01 250), oklch(0.32 0.025 256));
--color-tfa06-accedge: #8ba6e8;
--color-tfa06-accedge: light-dark(oklch(0.72 0.1 255), oklch(0.55 0.11 250));
--color-tfa06-tint: #eaeffc;
--color-tfa06-tint: light-dark(oklch(0.95 0.025 255), oklch(0.27 0.045 252));
--color-tfa06-acc: #3f5fd0;
--color-tfa06-acc: light-dark(oklch(0.48 0.19 260), oklch(0.8 0.12 250));
--color-tfa06-sun: #d99b2b;
--color-tfa06-sun: light-dark(oklch(0.68 0.14 75), oklch(0.78 0.14 75));
--color-tfa06-shadow: #d7dae6;
--color-tfa06-shadow: light-dark(oklch(0.3 0.05 255 / 0.1), oklch(0 0 0 / 0.4));
--font-tfa06-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Scheme default is the [color-scheme:light] utility on the root, NOT a rule here:
unlayered CSS beats Tailwind's utilities layer, so declaring it here would silently defeat
has-checked:[color-scheme:dark]. */
.tfa-06 {
width: 100%;
min-height: 100svh;
}
/* light-dark() tokens do not change computed value when color-scheme flips, so a CSS colour
transition on them freezes at the start colour in Chromium. The cross-fade is a View
Transition instead; these two lines just set its timing. */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.4,0,.2,1);
}
.tfa-06 [data-panel] {
interpolate-size: allow-keywords;
}
@supports (interpolate-size: allow-keywords) {
.tfa-06 details::details-content {
height: 0;
overflow: clip;
opacity: 0;
transition: height .38s cubic-bezier(.4,0,.2,1), opacity .3s, content-visibility .38s allow-discrete;
}
.tfa-06 details[open]::details-content {
height: auto;
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.tfa-06 details::details-content {
transition-duration: 1ms;
}
}
```
## JavaScript
```js
(function(){
var root=document.querySelector('.tfa-06');if(!root||root.dataset.tfa06)return;root.dataset.tfa06='1';
var cb=root.querySelector('#tfa06-mode'),mq=window.matchMedia('(prefers-color-scheme: dark)'),touched=false;
cb.checked=mq.matches;
var calm=window.matchMedia('(prefers-reduced-motion: reduce)');
cb.addEventListener('click',function(e){
touched=true;
if(!document.startViewTransition||calm.matches)return;
var next=cb.checked;e.preventDefault();
document.startViewTransition(function(){cb.checked=next;});
});
mq.addEventListener('change',function(e){if(!touched)cb.checked=e.matches;});
})();
```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: Tailwind Dark Mode FAQ Accordion with light-dark() and No .dark Class
Source: https://codefronts.com/snippets/tailwind-css-faq-accordion/tailwind-dark-mode-faq-accordion-light-dark/
The counter-argument to dark:. Every colour is declared once as a light-dark() pair inside @theme, and a single color-scheme flip re-skins the whole component — including native scrollbars, form controls and selection colours that a .dark class never reaches. A true two-way switch that starts from the visitor's OS preference, with zero duplicated utilities, a small seed script, and a View Transition cross-fade.
## HTML
```html
<section class="tfa-06 group/tfa06 w-full min-h-svh grid place-items-center bg-tfa06-bg font-tfa06-sans text-tfa06-ink px-5 py-12 [color-scheme:light] has-[#tfa06-mode:checked]:[color-scheme:dark]">
<div class="w-[min(660px,100%)]">
<div class="flex items-center justify-between gap-4">
<span class="text-[14px] font-extrabold tracking-[-0.01em]">Nightshift Docs</span>
<input id="tfa06-mode" type="checkbox" role="switch" class="peer absolute opacity-0 pointer-events-none">
<label for="tfa06-mode" title="Dark theme" class="flex min-h-11 cursor-pointer items-center gap-2 rounded-full border border-tfa06-edge bg-tfa06-panel px-2.5 py-1.5 text-tfa06-mut hover:border-tfa06-accedge peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-tfa06-acc">
<span aria-hidden="true" class="text-tfa06-sun"><svg viewBox="0 0 24 24" width="13" height="13"><circle cx="12" cy="12" r="4.4" fill="currentColor"/><g stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5 5l2.1 2.1M16.9 16.9L19 19M19 5l-2.1 2.1M7.1 16.9L5 19"/></g></svg></span>
<span aria-hidden="true" class="relative h-5 w-9 rounded-full bg-tfa06-edge group-has-[#tfa06-mode:checked]/tfa06:bg-tfa06-acc after:absolute after:left-0.5 after:top-0.5 after:size-4 after:rounded-full after:bg-tfa06-panel after:shadow-[0_1px_4px_var(--color-tfa06-shadow)] after:transition-transform after:duration-300 after:ease-[cubic-bezier(0.34,1.56,0.64,1)] group-has-[#tfa06-mode:checked]/tfa06:after:translate-x-4 motion-reduce:after:transition-none"></span>
<span aria-hidden="true" class="group-has-[#tfa06-mode:checked]/tfa06:text-tfa06-acc"><svg viewBox="0 0 24 24" width="13" height="13"><path d="M20 14.5A8.5 8.5 0 019.5 4a8.5 8.5 0 1010.5 10.5z" fill="currentColor"/></svg></span>
<span class="sr-only">Dark theme</span>
</label>
</div>
<header class="mt-7">
<h1 class="text-[clamp(28px,5vw,38px)] font-extrabold leading-[1.08] tracking-[-0.025em]">Frequently asked questions</h1>
<p class="mt-3 max-w-[54ch] text-[14.5px] leading-[1.7] text-tfa06-mut">Every colour below is one <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">light-dark()</code> token. Flip the switch — or your OS theme — and the whole component re-skins.</p>
</header>
<div data-panel class="mt-6 flex flex-col gap-2.5">
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge" open>
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Does dark mode save battery?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">On OLED screens, meaningfully — black pixels are unlit. On LCD panels the backlight runs regardless, so the gain is comfort, not power.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Why not just add a .dark class?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Class-swapping duplicates every colour rule and misses native UI: scrollbars, form controls and selection colours stay light. <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">color-scheme</code> re-skins those for free, and <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">light-dark()</code> keeps each token in one place.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">Should dark mode be pure black?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Usually no — near-black surfaces (this panel is oklch 0.22) reduce smearing on OLED and let shadows still read. Reserve true black for media canvases.</p>
</details>
<details class="group rounded-2xl border border-tfa06-edge bg-tfa06-panel shadow-[0_4px_16px_var(--color-tfa06-shadow)] open:border-tfa06-accedge">
<summary class="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 text-[15px] font-semibold hover:text-tfa06-acc focus-visible:rounded-2xl focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-tfa06-acc [&::-webkit-details-marker]:hidden">How do I persist the user's choice?
<i aria-hidden="true" class="ml-auto size-2.5 shrink-0 -translate-y-0.5 rotate-45 border-b-2 border-r-2 border-tfa06-mut transition-[rotate,translate] duration-350 group-hover:border-tfa06-acc group-open:translate-y-0.5 group-open:rotate-[225deg] group-open:border-tfa06-acc motion-reduce:transition-none"></i>
</summary>
<p class="max-w-[58ch] px-5 pb-4 text-[14px] leading-[1.7] text-tfa06-mut">Store one value ('light' / 'dark' / 'auto') in localStorage and restore it before first paint in a tiny inline script. The utilities need zero changes — you are only setting the checkbox.</p>
</details>
</div>
<p class="mt-6 text-[12.5px] text-tfa06-mut">Starts from your OS preference, then the switch wins both ways. Checked = <code class="rounded bg-tfa06-tint px-1.5 py-0.5 font-tfa06-mono text-[0.87em] text-tfa06-acc">has-[#tfa06-mode:checked]:[color-scheme:dark]</code>.</p>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
/* Every entry: a plain light value first (pre-2024 floor), then the light-dark() pair. */
--color-tfa06-bg: #f8f9fc;
--color-tfa06-bg: light-dark(oklch(0.975 0.008 250), oklch(0.17 0.02 258));
--color-tfa06-panel: #ffffff;
--color-tfa06-panel: light-dark(oklch(1 0 0), oklch(0.22 0.022 258));
--color-tfa06-ink: #2b2f3a;
--color-tfa06-ink: light-dark(oklch(0.22 0.025 258), oklch(0.96 0.006 250));
--color-tfa06-mut: #5f6472;
--color-tfa06-mut: light-dark(oklch(0.47 0.02 255), oklch(0.79 0.015 252));
--color-tfa06-edge: #e4e6ee;
--color-tfa06-edge: light-dark(oklch(0.91 0.01 250), oklch(0.32 0.025 256));
--color-tfa06-accedge: #8ba6e8;
--color-tfa06-accedge: light-dark(oklch(0.72 0.1 255), oklch(0.55 0.11 250));
--color-tfa06-tint: #eaeffc;
--color-tfa06-tint: light-dark(oklch(0.95 0.025 255), oklch(0.27 0.045 252));
--color-tfa06-acc: #3f5fd0;
--color-tfa06-acc: light-dark(oklch(0.48 0.19 260), oklch(0.8 0.12 250));
--color-tfa06-sun: #d99b2b;
--color-tfa06-sun: light-dark(oklch(0.68 0.14 75), oklch(0.78 0.14 75));
--color-tfa06-shadow: #d7dae6;
--color-tfa06-shadow: light-dark(oklch(0.3 0.05 255 / 0.1), oklch(0 0 0 / 0.4));
--font-tfa06-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
--font-tfa06-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Scheme default is the [color-scheme:light] utility on the root, NOT a rule here:
unlayered CSS beats Tailwind's utilities layer, so declaring it here would silently defeat
has-checked:[color-scheme:dark]. */
.tfa-06 {
width: 100%;
min-height: 100svh;
}
/* light-dark() tokens do not change computed value when color-scheme flips, so a CSS colour
transition on them freezes at the start colour in Chromium. The cross-fade is a View
Transition instead; these two lines just set its timing. */
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: .45s;
animation-timing-function: cubic-bezier(.4,0,.2,1);
}
.tfa-06 [data-panel] {
interpolate-size: allow-keywords;
}
@supports (interpolate-size: allow-keywords) {
.tfa-06 details::details-content {
height: 0;
overflow: clip;
opacity: 0;
transition: height .38s cubic-bezier(.4,0,.2,1), opacity .3s, content-visibility .38s allow-discrete;
}
.tfa-06 details[open]::details-content {
height: auto;
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.tfa-06 details::details-content {
transition-duration: 1ms;
}
}
```
## JavaScript
```js
(function(){
var root=document.querySelector('.tfa-06');if(!root||root.dataset.tfa06)return;root.dataset.tfa06='1';
var cb=root.querySelector('#tfa06-mode'),mq=window.matchMedia('(prefers-color-scheme: dark)'),touched=false;
cb.checked=mq.matches;
var calm=window.matchMedia('(prefers-reduced-motion: reduce)');
cb.addEventListener('click',function(e){
touched=true;
if(!document.startViewTransition||calm.matches)return;
var next=cb.checked;e.preventDefault();
document.startViewTransition(function(){cb.checked=next;});
});
mq.addEventListener('change',function(e){if(!touched)cb.checked=e.matches;});
})();
```How this works
Tokens once, scheme flips free. Each token carries both values; color-scheme decides which is live. In v4 this belongs in @theme, where it still mints utilities:
@theme {
--color-tfa06-panel: light-dark(oklch(1 0 0), oklch(0.22 0.022 258));
--color-tfa06-ink: light-dark(oklch(0.22 0.025 258), oklch(0.96 0.006 250));
}So bg-tfa06-panel is a single class that is white in light mode and near-black in dark — no dark:bg-* twin anywhere in the markup. Compare demo 05, which spends eleven dark: prefixes to do the same job: both are correct, and this is the one that scales as the palette grows.
The override is one variant. A hidden checkbox and two classes on the root — [color-scheme:light] and has-[#tfa06-mode:checked]:[color-scheme:dark]. A five-line script pre-checks the box from prefers-color-scheme, so the page opens in the OS theme and the switch can then force either direction. (Leaving the default at light dark is the trap: on a dark OS the unchecked state is already dark, so the switch appears to do nothing and light can never be forced.) Both must be utilities: a plain .tfa-06{color-scheme:…} rule is unlayered and would beat the variant, which is the classic reason a Tailwind v4 theme toggle silently does nothing.
Because the switch changes color-scheme rather than swapping classes, everything native follows automatically — scrollbars, the <details> internals, text selection, form controls. That is the part a class-based dark mode structurally cannot do.
Why the pairs live in @theme and not :root: a plain custom property would work for var() use, but it would not generate bg-*, text-* and border-* utilities. Tokens that want to be utilities go in @theme; tokens that must not (the counters in demo 04) stay out.
Make it yours
- Follow-OS only: delete the toggle, the script and both scheme classes, and use
[color-scheme:light_dark]— the pairs resolve from the OS with no control at all. - Three-way switch: swap the checkbox for radios and add a second
has-[...]:[color-scheme:light]rule alongside the dark one. - Persist the choice: write
cb.checkedto localStorage on change and prefer it over the OS value in the seed script. No CSS or utility changes. - Accent per scheme: the accent is a
light-dark()pair too — dark grounds need roughly +0.2 oklch lightness to hold 4.5:1.
Gotchas — read before shipping
light-dark()only works for COLOUR values. Shadows and gradients must compose a colour token viavar(), which is exactly what the shadow utility here does.- Without
color-schemeset,light-dark()always resolves to the light value — declaring the pairs is not enough; the scheme property is the switch. [color-scheme:dark]as an arbitrary property is version-proof; Tailwind's ownscheme-*utilities arrived later in the v4 line.- Never put
transition-colors(or any colour transition) on a surface painted withlight-dark()tokens. The computed value does not change whencolor-schemeflips, so Chromium keeps the start colour and you get light text on a stuck light background. Use a View Transition for the fade. - Test forced-dark on a light OS and vice versa. The classic bug is a hardcoded white shadow that only shows in one combination.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 129+ | not yet | not yet | 129+ |
Floor set by oklch(), interpolate-size as this demo is written. The core technique itself goes back further — Chrome 123+.
light-dark() shipped across all engines during 2024. Older browsers ignore the declaration and render the light theme, which stays fully legible — the @theme block declares a plain light value first for exactly that reason.