09 / 20
All 20 in 20 Tailwind CSS Sidebars
- 01Tailwind Accessible Sidebar Navigation ARIA Reference
- 02Tailwind Sidebar Focus Trap and Restore on Close
- 03Tailwind Inert Background Content Behind a Drawer
- 04Tailwind Sidebar Navigation Is Not a Tab List
- 05Tailwind Announcing Collapsed and Expanded Sidebar State
- 06Tailwind Roving Tabindex for Sidebar Keyboard Navigation
- 07Tailwind Persisting Sidebar Collapse State Across Reloads
- 08Tailwind Sidebar Scroll Containment with Overscroll Behavior
- 09Tailwind Nested Submenu Keyboard and Screen Reader Behavior
- 10Tailwind Resizable Sidebar with a Keyboard Accessible Handle
- 11Pure Tailwind Collapsible Sidebar with Checkbox State
- 12Tailwind Responsive Sidebar Breakpoint Behavior
- 13Pure Tailwind Icon Rail Sidebar with Accessible Labels
- 14Tailwind Dashboard Sidebar with Current Page State
- 15Pure Tailwind Multi Level Sidebar Menu Disclosure Pattern
- 16Pure Tailwind Accordion Sidebar with Details and Summary
- 17Pure Tailwind Sticky Full Height Sidebar Layout Mechanics
- 18Pure Tailwind Floating Sidebar Stacking and Overlay Order
- 19Tailwind Off Canvas Sidebar Drawer with Focus Return
- 20Pure Tailwind Sidebar Footer Pinning with Scrollable Nav
Tailwind Nested Submenu Keyboard and Screen Reader Behavior
Two-level navigation done natively: each group is a <details>, so a collapsed submenu is genuinely removed from the tab order and the expanded state is exposed by the browser. The port's real find is group-has-[a[aria-current=page]]: — a group that contains the current page marks itself, both with a dot and with a visually hidden phrase, entirely from the class list.
Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69
Cruip · Mosaic by Cruip · Affiliate link
The code
<section class="tsb-09 block relative w-full min-h-svh bg-tsb09-bg text-tsb09-ink font-tsb09-sans leading-[1.55]" aria-label="Fern CMS">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-09-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-09-theme">
<label class="grid place-items-center w-11 h-11 rounded-[0.5rem] border border-tsb09-line bg-tsb09-surf text-tsb09-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb09-acc peer-focus-visible:outline-offset-2" for="tsb-09-theme">
<svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
</label>
</div>
<div class="grid grid-cols-[minmax(0,1fr)] min-h-svh bg-tsb09-surf r46:grid-cols-[minmax(0,18.5rem)_minmax(0,1fr)]">
<nav class="min-w-0 px-2.5 pt-4 pb-5 bg-tsb09-surf2 border-b border-tsb09-line r46:border-b-0 r46:border-r r46:border-tsb09-line" aria-label="Fern workspace">
<p class="m-0 mb-3 px-2.5 text-[0.74rem] tracking-[0.08em] uppercase text-tsb09-mut">Fern — Meridian site</p>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Dashboard</a></li>
</ul>
<details class="group my-0.5" open>
<summary class="flex items-center gap-2 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] font-semibold cursor-pointer list-none [&::-webkit-details-marker]:hidden hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2">
<svg class="w-4 h-4 flex-none text-tsb09-mut transition-transform duration-150 ease-out group-open:rotate-90 motion-reduce:transition-none" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Content
<span class="hidden group-has-[a[aria-current=page]]:block absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">contains the current page</span>
<span class="hidden group-has-[a[aria-current=page]]:block flex-none ml-auto w-1.5 h-1.5 rounded-full bg-tsb09-acc" aria-hidden="true"></span>
</summary>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Pages</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main" aria-current="page">Articles</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Media library</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Redirects</a></li>
</ul>
</details>
<details class="group my-0.5">
<summary class="flex items-center gap-2 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] font-semibold cursor-pointer list-none [&::-webkit-details-marker]:hidden hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2">
<svg class="w-4 h-4 flex-none text-tsb09-mut transition-transform duration-150 ease-out group-open:rotate-90 motion-reduce:transition-none" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Team settings
<span class="hidden group-has-[a[aria-current=page]]:block absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">contains the current page</span>
<span class="hidden group-has-[a[aria-current=page]]:block flex-none ml-auto w-1.5 h-1.5 rounded-full bg-tsb09-acc" aria-hidden="true"></span>
</summary>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Members</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Roles</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Audit log</a></li>
</ul>
</details>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">API keys</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Billing</a></li>
</ul>
</nav>
<main class="min-w-0 content-start px-5 pt-6 pb-10 r46:px-9 r46:pt-10 r46:pb-12" id="tsb-09-main">
<p class="m-0 mb-1 text-[0.78rem] text-tsb09-mut">Content / Articles</p>
<h2 class="m-0 mb-5 font-tsb09-display text-[clamp(1.35rem,3.4vw,1.9rem)] leading-[1.15] tracking-[-0.015em]">Articles</h2>
<table class="w-full max-w-[48rem] border-collapse text-[0.9rem]">
<caption class="text-start pb-2 text-[0.74rem] tracking-[0.08em] uppercase text-tsb09-mut">Recently edited</caption>
<thead><tr class="[&>th]:p-2.5 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb09-line [&>th]:text-[0.74rem] [&>th]:tracking-[0.06em] [&>th]:uppercase [&>th]:font-semibold [&>th]:text-tsb09-mut"><th scope="col">Title</th><th scope="col">Editor</th><th scope="col">State</th></tr></thead>
<tbody class="[&>tr>td]:p-2.5 [&>tr>td]:border-b [&>tr>td]:border-tsb09-line [&>tr>td]:min-w-0">
<tr><td>Deployment regions</td><td>Alex Chen</td><td>Published</td></tr>
<tr><td>Token rotation policy</td><td>Priya Sharma</td><td>In review</td></tr>
<tr><td>Audit log retention</td><td>Sam Rivera</td><td>Draft</td></tr>
<tr><td>Media naming rules</td><td>Ada Okafor</td><td>Published</td></tr>
</tbody>
</table>
</main>
</div>
</section><section class="tsb-09 block relative w-full min-h-svh bg-tsb09-bg text-tsb09-ink font-tsb09-sans leading-[1.55]" aria-label="Fern CMS">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-09-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-09-theme">
<label class="grid place-items-center w-11 h-11 rounded-[0.5rem] border border-tsb09-line bg-tsb09-surf text-tsb09-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb09-acc peer-focus-visible:outline-offset-2" for="tsb-09-theme">
<svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
</label>
</div>
<div class="grid grid-cols-[minmax(0,1fr)] min-h-svh bg-tsb09-surf r46:grid-cols-[minmax(0,18.5rem)_minmax(0,1fr)]">
<nav class="min-w-0 px-2.5 pt-4 pb-5 bg-tsb09-surf2 border-b border-tsb09-line r46:border-b-0 r46:border-r r46:border-tsb09-line" aria-label="Fern workspace">
<p class="m-0 mb-3 px-2.5 text-[0.74rem] tracking-[0.08em] uppercase text-tsb09-mut">Fern — Meridian site</p>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Dashboard</a></li>
</ul>
<details class="group my-0.5" open>
<summary class="flex items-center gap-2 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] font-semibold cursor-pointer list-none [&::-webkit-details-marker]:hidden hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2">
<svg class="w-4 h-4 flex-none text-tsb09-mut transition-transform duration-150 ease-out group-open:rotate-90 motion-reduce:transition-none" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Content
<span class="hidden group-has-[a[aria-current=page]]:block absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">contains the current page</span>
<span class="hidden group-has-[a[aria-current=page]]:block flex-none ml-auto w-1.5 h-1.5 rounded-full bg-tsb09-acc" aria-hidden="true"></span>
</summary>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Pages</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main" aria-current="page">Articles</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Media library</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Redirects</a></li>
</ul>
</details>
<details class="group my-0.5">
<summary class="flex items-center gap-2 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] font-semibold cursor-pointer list-none [&::-webkit-details-marker]:hidden hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2">
<svg class="w-4 h-4 flex-none text-tsb09-mut transition-transform duration-150 ease-out group-open:rotate-90 motion-reduce:transition-none" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Team settings
<span class="hidden group-has-[a[aria-current=page]]:block absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">contains the current page</span>
<span class="hidden group-has-[a[aria-current=page]]:block flex-none ml-auto w-1.5 h-1.5 rounded-full bg-tsb09-acc" aria-hidden="true"></span>
</summary>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Members</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Roles</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Audit log</a></li>
</ul>
</details>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">API keys</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Billing</a></li>
</ul>
</nav>
<main class="min-w-0 content-start px-5 pt-6 pb-10 r46:px-9 r46:pt-10 r46:pb-12" id="tsb-09-main">
<p class="m-0 mb-1 text-[0.78rem] text-tsb09-mut">Content / Articles</p>
<h2 class="m-0 mb-5 font-tsb09-display text-[clamp(1.35rem,3.4vw,1.9rem)] leading-[1.15] tracking-[-0.015em]">Articles</h2>
<table class="w-full max-w-[48rem] border-collapse text-[0.9rem]">
<caption class="text-start pb-2 text-[0.74rem] tracking-[0.08em] uppercase text-tsb09-mut">Recently edited</caption>
<thead><tr class="[&>th]:p-2.5 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb09-line [&>th]:text-[0.74rem] [&>th]:tracking-[0.06em] [&>th]:uppercase [&>th]:font-semibold [&>th]:text-tsb09-mut"><th scope="col">Title</th><th scope="col">Editor</th><th scope="col">State</th></tr></thead>
<tbody class="[&>tr>td]:p-2.5 [&>tr>td]:border-b [&>tr>td]:border-tsb09-line [&>tr>td]:min-w-0">
<tr><td>Deployment regions</td><td>Alex Chen</td><td>Published</td></tr>
<tr><td>Token rotation policy</td><td>Priya Sharma</td><td>In review</td></tr>
<tr><td>Audit log retention</td><td>Sam Rivera</td><td>Draft</td></tr>
<tr><td>Media naming rules</td><td>Ada Okafor</td><td>Published</td></tr>
</tbody>
</table>
</main>
</div>
</section>@import "tailwindcss";
@theme {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-bg: oklch(96.5% 0.01 258);
--color-tsb09-surf: #ffffff;
--color-tsb09-surf: oklch(100% 0 0);
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-surf2: oklch(94% 0.015 258);
--color-tsb09-line: #d2dcec;
--color-tsb09-line: oklch(87% 0.02 258);
--color-tsb09-ink: #101a2b;
--color-tsb09-ink: oklch(19% 0.03 258);
--color-tsb09-mut: #56658a;
--color-tsb09-mut: oklch(47% 0.04 258);
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
--color-tsb09-accsoft: oklch(93% 0.03 258);
--font-tsb09-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb09-display: 'Manrope', 'Segoe UI', system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. */
.tsb-09 {
width: 100%;
min-height: 100svh;
}
.tsb-09 *,
.tsb-09 *::before,
.tsb-09 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
@media (prefers-color-scheme: dark) {
.tsb-09 {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-surf: #ffffff;
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-line: #d2dcec;
--color-tsb09-ink: #101a2b;
--color-tsb09-mut: #56658a;
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
}
}
@media (prefers-color-scheme: light) {
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
}@import "tailwindcss";
@theme {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-bg: oklch(96.5% 0.01 258);
--color-tsb09-surf: #ffffff;
--color-tsb09-surf: oklch(100% 0 0);
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-surf2: oklch(94% 0.015 258);
--color-tsb09-line: #d2dcec;
--color-tsb09-line: oklch(87% 0.02 258);
--color-tsb09-ink: #101a2b;
--color-tsb09-ink: oklch(19% 0.03 258);
--color-tsb09-mut: #56658a;
--color-tsb09-mut: oklch(47% 0.04 258);
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
--color-tsb09-accsoft: oklch(93% 0.03 258);
--font-tsb09-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb09-display: 'Manrope', 'Segoe UI', system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. */
.tsb-09 {
width: 100%;
min-height: 100svh;
}
.tsb-09 *,
.tsb-09 *::before,
.tsb-09 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
@media (prefers-color-scheme: dark) {
.tsb-09 {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-surf: #ffffff;
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-line: #d2dcec;
--color-tsb09-ink: #101a2b;
--color-tsb09-mut: #56658a;
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
}
}
@media (prefers-color-scheme: light) {
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
}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-tsb09-bg: #f4f7fc;
--color-tsb09-bg: oklch(96.5% 0.01 258);
--color-tsb09-surf: #ffffff;
--color-tsb09-surf: oklch(100% 0 0);
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-surf2: oklch(94% 0.015 258);
--color-tsb09-line: #d2dcec;
--color-tsb09-line: oklch(87% 0.02 258);
--color-tsb09-ink: #101a2b;
--color-tsb09-ink: oklch(19% 0.03 258);
--color-tsb09-mut: #56658a;
--color-tsb09-mut: oklch(47% 0.04 258);
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
--color-tsb09-accsoft: oklch(93% 0.03 258);
--font-tsb09-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb09-display: 'Manrope', 'Segoe UI', system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. */
.tsb-09 {
width: 100%;
min-height: 100svh;
}
.tsb-09 *,
.tsb-09 *::before,
.tsb-09 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
@media (prefers-color-scheme: dark) {
.tsb-09 {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-surf: #ffffff;
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-line: #d2dcec;
--color-tsb09-ink: #101a2b;
--color-tsb09-mut: #56658a;
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
}
}
@media (prefers-color-scheme: light) {
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
}@import "tailwindcss";
@theme {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-bg: oklch(96.5% 0.01 258);
--color-tsb09-surf: #ffffff;
--color-tsb09-surf: oklch(100% 0 0);
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-surf2: oklch(94% 0.015 258);
--color-tsb09-line: #d2dcec;
--color-tsb09-line: oklch(87% 0.02 258);
--color-tsb09-ink: #101a2b;
--color-tsb09-ink: oklch(19% 0.03 258);
--color-tsb09-mut: #56658a;
--color-tsb09-mut: oklch(47% 0.04 258);
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
--color-tsb09-accsoft: oklch(93% 0.03 258);
--font-tsb09-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb09-display: 'Manrope', 'Segoe UI', system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. */
.tsb-09 {
width: 100%;
min-height: 100svh;
}
.tsb-09 *,
.tsb-09 *::before,
.tsb-09 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
@media (prefers-color-scheme: dark) {
.tsb-09 {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-surf: #ffffff;
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-line: #d2dcec;
--color-tsb09-ink: #101a2b;
--color-tsb09-mut: #56658a;
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
}
}
@media (prefers-color-scheme: light) {
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
}<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=Manrope:wght@400;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=Manrope:wght@400;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
.group:has([aria-current="page"]) > .summarybecomesgroup-has-[a[aria-current=page]]:on the two marker spans. The condition and its two consequences now sit inside the summary they describe.- The CSS version drew the dot with
::afterand acontentstring. In utilities a real<span>is clearer: it lives next to the visually hidden phrase that announces the same fact, so the visual and announced markers cannot be changed independently. group-open:rotate-90is used here and explicitly flagged as unsafe under nesting — with nested<details>it matches through every open ancestor. This tree is flat by design.hiddenplus agroup-has-[…]:blockoverride is the right pairing for a marker: absent from the DOM box tree until the condition holds, rather than present-and-transparent.- Sub-level links carry
pl-8.5instead of a--submodifier class, which removes the whole BEM-modifier layer from the port. - Zero JS is not a stylistic choice in this demo —
<details>plus:has()covers the entire behaviour, which is the argument the collection is making about v4's state variants.
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 Nested Submenu Keyboard and Screen Reader Behavior
Source: https://codefronts.com/navigation/tailwind-css-sidebars/tailwind-nested-submenu-keyboard-and-screen-reader-behavior/
Two-level navigation done natively: each group is a <details>, so a collapsed submenu is genuinely removed from the tab order and the expanded state is exposed by the browser. The port's real find is group-has-[a[aria-current=page]]: — a group that contains the current page marks itself, both with a dot and with a visually hidden phrase, entirely from the class list.
## HTML
```html
<section class="tsb-09 block relative w-full min-h-svh bg-tsb09-bg text-tsb09-ink font-tsb09-sans leading-[1.55]" aria-label="Fern CMS">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-09-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-09-theme">
<label class="grid place-items-center w-11 h-11 rounded-[0.5rem] border border-tsb09-line bg-tsb09-surf text-tsb09-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb09-acc peer-focus-visible:outline-offset-2" for="tsb-09-theme">
<svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
</label>
</div>
<div class="grid grid-cols-[minmax(0,1fr)] min-h-svh bg-tsb09-surf r46:grid-cols-[minmax(0,18.5rem)_minmax(0,1fr)]">
<nav class="min-w-0 px-2.5 pt-4 pb-5 bg-tsb09-surf2 border-b border-tsb09-line r46:border-b-0 r46:border-r r46:border-tsb09-line" aria-label="Fern workspace">
<p class="m-0 mb-3 px-2.5 text-[0.74rem] tracking-[0.08em] uppercase text-tsb09-mut">Fern — Meridian site</p>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Dashboard</a></li>
</ul>
<details class="group my-0.5" open>
<summary class="flex items-center gap-2 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] font-semibold cursor-pointer list-none [&::-webkit-details-marker]:hidden hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2">
<svg class="w-4 h-4 flex-none text-tsb09-mut transition-transform duration-150 ease-out group-open:rotate-90 motion-reduce:transition-none" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Content
<span class="hidden group-has-[a[aria-current=page]]:block absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">contains the current page</span>
<span class="hidden group-has-[a[aria-current=page]]:block flex-none ml-auto w-1.5 h-1.5 rounded-full bg-tsb09-acc" aria-hidden="true"></span>
</summary>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Pages</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main" aria-current="page">Articles</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Media library</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Redirects</a></li>
</ul>
</details>
<details class="group my-0.5">
<summary class="flex items-center gap-2 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] font-semibold cursor-pointer list-none [&::-webkit-details-marker]:hidden hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2">
<svg class="w-4 h-4 flex-none text-tsb09-mut transition-transform duration-150 ease-out group-open:rotate-90 motion-reduce:transition-none" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Team settings
<span class="hidden group-has-[a[aria-current=page]]:block absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">contains the current page</span>
<span class="hidden group-has-[a[aria-current=page]]:block flex-none ml-auto w-1.5 h-1.5 rounded-full bg-tsb09-acc" aria-hidden="true"></span>
</summary>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Members</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Roles</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Audit log</a></li>
</ul>
</details>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">API keys</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Billing</a></li>
</ul>
</nav>
<main class="min-w-0 content-start px-5 pt-6 pb-10 r46:px-9 r46:pt-10 r46:pb-12" id="tsb-09-main">
<p class="m-0 mb-1 text-[0.78rem] text-tsb09-mut">Content / Articles</p>
<h2 class="m-0 mb-5 font-tsb09-display text-[clamp(1.35rem,3.4vw,1.9rem)] leading-[1.15] tracking-[-0.015em]">Articles</h2>
<table class="w-full max-w-[48rem] border-collapse text-[0.9rem]">
<caption class="text-start pb-2 text-[0.74rem] tracking-[0.08em] uppercase text-tsb09-mut">Recently edited</caption>
<thead><tr class="[&>th]:p-2.5 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb09-line [&>th]:text-[0.74rem] [&>th]:tracking-[0.06em] [&>th]:uppercase [&>th]:font-semibold [&>th]:text-tsb09-mut"><th scope="col">Title</th><th scope="col">Editor</th><th scope="col">State</th></tr></thead>
<tbody class="[&>tr>td]:p-2.5 [&>tr>td]:border-b [&>tr>td]:border-tsb09-line [&>tr>td]:min-w-0">
<tr><td>Deployment regions</td><td>Alex Chen</td><td>Published</td></tr>
<tr><td>Token rotation policy</td><td>Priya Sharma</td><td>In review</td></tr>
<tr><td>Audit log retention</td><td>Sam Rivera</td><td>Draft</td></tr>
<tr><td>Media naming rules</td><td>Ada Okafor</td><td>Published</td></tr>
</tbody>
</table>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-bg: oklch(96.5% 0.01 258);
--color-tsb09-surf: #ffffff;
--color-tsb09-surf: oklch(100% 0 0);
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-surf2: oklch(94% 0.015 258);
--color-tsb09-line: #d2dcec;
--color-tsb09-line: oklch(87% 0.02 258);
--color-tsb09-ink: #101a2b;
--color-tsb09-ink: oklch(19% 0.03 258);
--color-tsb09-mut: #56658a;
--color-tsb09-mut: oklch(47% 0.04 258);
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
--color-tsb09-accsoft: oklch(93% 0.03 258);
--font-tsb09-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb09-display: 'Manrope', 'Segoe UI', system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. */
.tsb-09 {
width: 100%;
min-height: 100svh;
}
.tsb-09 *,
.tsb-09 *::before,
.tsb-09 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
@media (prefers-color-scheme: dark) {
.tsb-09 {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-surf: #ffffff;
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-line: #d2dcec;
--color-tsb09-ink: #101a2b;
--color-tsb09-mut: #56658a;
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
}
}
@media (prefers-color-scheme: light) {
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
}
```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 Nested Submenu Keyboard and Screen Reader Behavior
Source: https://codefronts.com/navigation/tailwind-css-sidebars/tailwind-nested-submenu-keyboard-and-screen-reader-behavior/
Two-level navigation done natively: each group is a <details>, so a collapsed submenu is genuinely removed from the tab order and the expanded state is exposed by the browser. The port's real find is group-has-[a[aria-current=page]]: — a group that contains the current page marks itself, both with a dot and with a visually hidden phrase, entirely from the class list.
## HTML
```html
<section class="tsb-09 block relative w-full min-h-svh bg-tsb09-bg text-tsb09-ink font-tsb09-sans leading-[1.55]" aria-label="Fern CMS">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-09-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-09-theme">
<label class="grid place-items-center w-11 h-11 rounded-[0.5rem] border border-tsb09-line bg-tsb09-surf text-tsb09-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb09-acc peer-focus-visible:outline-offset-2" for="tsb-09-theme">
<svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
</label>
</div>
<div class="grid grid-cols-[minmax(0,1fr)] min-h-svh bg-tsb09-surf r46:grid-cols-[minmax(0,18.5rem)_minmax(0,1fr)]">
<nav class="min-w-0 px-2.5 pt-4 pb-5 bg-tsb09-surf2 border-b border-tsb09-line r46:border-b-0 r46:border-r r46:border-tsb09-line" aria-label="Fern workspace">
<p class="m-0 mb-3 px-2.5 text-[0.74rem] tracking-[0.08em] uppercase text-tsb09-mut">Fern — Meridian site</p>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Dashboard</a></li>
</ul>
<details class="group my-0.5" open>
<summary class="flex items-center gap-2 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] font-semibold cursor-pointer list-none [&::-webkit-details-marker]:hidden hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2">
<svg class="w-4 h-4 flex-none text-tsb09-mut transition-transform duration-150 ease-out group-open:rotate-90 motion-reduce:transition-none" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Content
<span class="hidden group-has-[a[aria-current=page]]:block absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">contains the current page</span>
<span class="hidden group-has-[a[aria-current=page]]:block flex-none ml-auto w-1.5 h-1.5 rounded-full bg-tsb09-acc" aria-hidden="true"></span>
</summary>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Pages</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main" aria-current="page">Articles</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Media library</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb09-accsoft aria-[current=page]:text-tsb09-acc aria-[current=page]:font-semibold" href="#tsb-09-main">Redirects</a></li>
</ul>
</details>
<details class="group my-0.5">
<summary class="flex items-center gap-2 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] font-semibold cursor-pointer list-none [&::-webkit-details-marker]:hidden hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2">
<svg class="w-4 h-4 flex-none text-tsb09-mut transition-transform duration-150 ease-out group-open:rotate-90 motion-reduce:transition-none" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>
Team settings
<span class="hidden group-has-[a[aria-current=page]]:block absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">contains the current page</span>
<span class="hidden group-has-[a[aria-current=page]]:block flex-none ml-auto w-1.5 h-1.5 rounded-full bg-tsb09-acc" aria-hidden="true"></span>
</summary>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Members</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Roles</a></li>
<li><a class="flex items-center min-w-0 min-h-11 py-2 pr-2.5 pl-8.5 rounded-[0.4rem] text-[0.89rem] text-tsb09-mut no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Audit log</a></li>
</ul>
</details>
<ul class="list-none m-0 p-0">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">API keys</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.4rem] text-[0.92rem] text-tsb09-ink no-underline hover:bg-tsb09-surf focus-visible:outline-2 focus-visible:outline-tsb09-acc focus-visible:-outline-offset-2" href="#tsb-09-main">Billing</a></li>
</ul>
</nav>
<main class="min-w-0 content-start px-5 pt-6 pb-10 r46:px-9 r46:pt-10 r46:pb-12" id="tsb-09-main">
<p class="m-0 mb-1 text-[0.78rem] text-tsb09-mut">Content / Articles</p>
<h2 class="m-0 mb-5 font-tsb09-display text-[clamp(1.35rem,3.4vw,1.9rem)] leading-[1.15] tracking-[-0.015em]">Articles</h2>
<table class="w-full max-w-[48rem] border-collapse text-[0.9rem]">
<caption class="text-start pb-2 text-[0.74rem] tracking-[0.08em] uppercase text-tsb09-mut">Recently edited</caption>
<thead><tr class="[&>th]:p-2.5 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb09-line [&>th]:text-[0.74rem] [&>th]:tracking-[0.06em] [&>th]:uppercase [&>th]:font-semibold [&>th]:text-tsb09-mut"><th scope="col">Title</th><th scope="col">Editor</th><th scope="col">State</th></tr></thead>
<tbody class="[&>tr>td]:p-2.5 [&>tr>td]:border-b [&>tr>td]:border-tsb09-line [&>tr>td]:min-w-0">
<tr><td>Deployment regions</td><td>Alex Chen</td><td>Published</td></tr>
<tr><td>Token rotation policy</td><td>Priya Sharma</td><td>In review</td></tr>
<tr><td>Audit log retention</td><td>Sam Rivera</td><td>Draft</td></tr>
<tr><td>Media naming rules</td><td>Ada Okafor</td><td>Published</td></tr>
</tbody>
</table>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-bg: oklch(96.5% 0.01 258);
--color-tsb09-surf: #ffffff;
--color-tsb09-surf: oklch(100% 0 0);
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-surf2: oklch(94% 0.015 258);
--color-tsb09-line: #d2dcec;
--color-tsb09-line: oklch(87% 0.02 258);
--color-tsb09-ink: #101a2b;
--color-tsb09-ink: oklch(19% 0.03 258);
--color-tsb09-mut: #56658a;
--color-tsb09-mut: oklch(47% 0.04 258);
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
--color-tsb09-accsoft: oklch(93% 0.03 258);
--font-tsb09-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb09-display: 'Manrope', 'Segoe UI', system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
w-full min-h-svh. */
.tsb-09 {
width: 100%;
min-height: 100svh;
}
.tsb-09 *,
.tsb-09 *::before,
.tsb-09 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
@media (prefers-color-scheme: dark) {
.tsb-09 {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #f4f7fc;
--color-tsb09-surf: #ffffff;
--color-tsb09-surf2: #eaf0f9;
--color-tsb09-line: #d2dcec;
--color-tsb09-ink: #101a2b;
--color-tsb09-mut: #56658a;
--color-tsb09-acc: #1554c8;
--color-tsb09-acc: oklch(48% 0.17 258);
--color-tsb09-accsoft: #dfe9fc;
}
}
@media (prefers-color-scheme: light) {
.tsb-09:has(.tsb-09-theme-input:checked) {
--color-tsb09-bg: #080d16;
--color-tsb09-surf: #0f1725;
--color-tsb09-surf2: #16202f;
--color-tsb09-line: #24303f;
--color-tsb09-ink: #e9eef8;
--color-tsb09-mut: #92a1bd;
--color-tsb09-acc: #7fa6ff;
--color-tsb09-acc: oklch(78% 0.13 260);
--color-tsb09-accsoft: #172441;
}
}
```How this works
Collapsed means gone, and that is the whole reason to use <details>. A submenu hidden with opacity-0 or h-0 is still focusable, so a keyboard user tabs into links they cannot see. <details> removes its content from the tree and the tab order, exposes expanded/collapsed to screen readers, and needs no aria-expanded bookkeeping.
The group that holds the current page says so. When a submenu is collapsed, nothing indicates that the page you are on lives inside it. :has() solves it, and in utilities it reads as a variant:
<details class="group">
<summary>Content
<span class="hidden group-has-[a[aria-current=page]]:block
absolute w-px h-px [clip-path:inset(50%)]">contains the current page</span>
<span class="hidden group-has-[a[aria-current=page]]:block
ml-auto w-1.5 h-1.5 rounded-full bg-tsb09-acc"></span>One visible dot, one announced phrase, both keyed to the same condition.
Two levels, on purpose. Three-deep navigation is where disclosure trees stop being usable — the fix is almost always flattening the information architecture, not more chevrons.
The dot is a real element, not a pseudo-element. Tailwind can do after: utilities, but a marker that carries meaning is easier to keep honest as a sibling of the phrase that announces it.
Make it yours
- Ship a group open by default with the
openattribute — the group containing the current page is the obvious candidate, and it is what this demo does. - Make the groups mutually exclusive by giving each
<details>the samenameattribute: native exclusive accordion, no script. - Swap the marker dot for a count, a bold summary or a left rule; the
group-has-[…]condition is independent of what it styles. - Retheme through
--color-tsb09-accand--color-tsb09-accsoft. - Change the indent depth in the sub-links'
pl-8.5. - If a group must animate open, move to a scripted button with
aria-expanded— demo 15 covers the animated two-level version.
Gotchas — read before shipping
group-open:matches through EVERY open ancestor. With nested<details>it rotates the closed inner chevron too — use a direct-child arbitrary variant there. It is safe in this demo only because the tree is flat.- Hiding a submenu with
invisible,opacity-0orh-0leaves it focusable.<details>orhiddenare the only honest closed states. group-has-[a[aria-current=page]]:needs the attribute selector spelled out; the baregroup-has-aria-current:form tests="true"and will not match a link markedpage.- Do not put
aria-expandedon the<summary>. The browser already exposes the state, and the duplicate is announced twice. - A marker that is only a colour or only a dot is invisible to a screen-reader user; the visually hidden phrase is the half people forget.
- Three levels of disclosure in a sidebar is a signal to flatten the IA, not to add another variant.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
:has() — which group-has-* compiles to — is Baseline 2023 and shipped in Firefox 121; Tailwind v4's own floor is higher. Engines without :has() lose the marker dot and its announcement, and nothing else.