06 / 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 Roving Tabindex for Sidebar Keyboard Navigation
Ten folders means ten tab stops before a keyboard user reaches the content — unless the list is one stop and the arrow keys move inside it. Roving tabindex is script by definition (it rewrites tabindex as focus moves), so the port's interesting half is the focus treatment: focus-visible: utilities give the moved-to item a ring AND a tint, because with one tab stop the focus indicator is the only thing telling the user where they are.
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-06 block relative w-full min-h-svh bg-tsb06-bg text-tsb06-ink font-tsb06-sans leading-[1.55]" aria-label="Ranger file manager">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-06-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-06-theme">
<label class="grid place-items-center w-11 h-11 rounded-[0.4rem] border border-tsb06-line bg-tsb06-surf text-tsb06-ink cursor-pointer shadow-[inset_0_1px_3px_oklch(25%_0.02_140/0.14)] peer-focus-visible:outline-2 peer-focus-visible:outline-tsb06-acc peer-focus-visible:outline-offset-2" for="tsb-06-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 r46:grid-cols-[17rem_minmax(0,1fr)]">
<nav class="min-w-0 px-3 py-4 bg-tsb06-surf border-b border-tsb06-line shadow-[inset_0_2px_6px_oklch(25%_0.02_140/0.09)] r46:border-b-0 r46:border-r r46:border-tsb06-line" aria-label="Ranger folders">
<p class="m-0 mb-2 px-2.5 text-[0.72rem] tracking-[0.1em] uppercase text-tsb06-mut">All folders</p>
<ul class="list-none m-0 p-0 grid gap-px" id="tsb-06-roving">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft aria-[current=page]:bg-tsb06-accsoft aria-[current=page]:text-tsb06-acc aria-[current=page]:font-semibold" href="#tsb-06-main" tabindex="0" aria-current="page">Deployments</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Design source</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Contracts 2026</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Audit exports</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Vendor invoices</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Brand assets</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Field photos</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Board minutes</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Payroll archive</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Trash</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-06-main">
<h2 class="m-0 mb-1 font-tsb06-display text-[clamp(1.4rem,3.6vw,2rem)] leading-[1.1]">Deployments</h2>
<p class="m-0 mb-5 text-[0.88rem] text-tsb06-mut">42 items · last change 2 hours ago</p>
<table class="w-full border-collapse overflow-hidden text-[0.9rem] bg-tsb06-surf border border-tsb06-line rounded-[0.5rem]">
<thead><tr class="[&>th]:px-3.5 [&>th]:py-2.5 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb06-line [&>th]:text-[0.72rem] [&>th]:tracking-[0.1em] [&>th]:uppercase [&>th]:font-bold [&>th]:text-tsb06-mut"><th scope="col">Name</th><th scope="col">Owner</th><th scope="col">Size</th></tr></thead>
<tbody class="[&>tr>td]:px-3.5 [&>tr>td]:py-2.5 [&>tr>td]:border-b [&>tr>td]:border-tsb06-line [&>tr:last-child>td]:border-b-0 [&>tr>td:last-child]:text-tsb06-mut [&>tr>td:last-child]:tabular-nums">
<tr><td>release-notes.md</td><td>Priya Sharma</td><td>18 KB</td></tr>
<tr><td>rollback-runbook.pdf</td><td>Jordan Lee</td><td>1.2 MB</td></tr>
<tr><td>env-matrix.csv</td><td>Sam Rivera</td><td>44 KB</td></tr>
<tr><td>token-exchange.log</td><td>Ada Okafor</td><td>7.8 MB</td></tr>
<tr><td>staging-snapshot.zip</td><td>Tomas Vidal</td><td>212 MB</td></tr>
</tbody>
</table>
</main>
</div>
</section><section class="tsb-06 block relative w-full min-h-svh bg-tsb06-bg text-tsb06-ink font-tsb06-sans leading-[1.55]" aria-label="Ranger file manager">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-06-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-06-theme">
<label class="grid place-items-center w-11 h-11 rounded-[0.4rem] border border-tsb06-line bg-tsb06-surf text-tsb06-ink cursor-pointer shadow-[inset_0_1px_3px_oklch(25%_0.02_140/0.14)] peer-focus-visible:outline-2 peer-focus-visible:outline-tsb06-acc peer-focus-visible:outline-offset-2" for="tsb-06-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 r46:grid-cols-[17rem_minmax(0,1fr)]">
<nav class="min-w-0 px-3 py-4 bg-tsb06-surf border-b border-tsb06-line shadow-[inset_0_2px_6px_oklch(25%_0.02_140/0.09)] r46:border-b-0 r46:border-r r46:border-tsb06-line" aria-label="Ranger folders">
<p class="m-0 mb-2 px-2.5 text-[0.72rem] tracking-[0.1em] uppercase text-tsb06-mut">All folders</p>
<ul class="list-none m-0 p-0 grid gap-px" id="tsb-06-roving">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft aria-[current=page]:bg-tsb06-accsoft aria-[current=page]:text-tsb06-acc aria-[current=page]:font-semibold" href="#tsb-06-main" tabindex="0" aria-current="page">Deployments</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Design source</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Contracts 2026</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Audit exports</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Vendor invoices</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Brand assets</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Field photos</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Board minutes</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Payroll archive</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Trash</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-06-main">
<h2 class="m-0 mb-1 font-tsb06-display text-[clamp(1.4rem,3.6vw,2rem)] leading-[1.1]">Deployments</h2>
<p class="m-0 mb-5 text-[0.88rem] text-tsb06-mut">42 items · last change 2 hours ago</p>
<table class="w-full border-collapse overflow-hidden text-[0.9rem] bg-tsb06-surf border border-tsb06-line rounded-[0.5rem]">
<thead><tr class="[&>th]:px-3.5 [&>th]:py-2.5 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb06-line [&>th]:text-[0.72rem] [&>th]:tracking-[0.1em] [&>th]:uppercase [&>th]:font-bold [&>th]:text-tsb06-mut"><th scope="col">Name</th><th scope="col">Owner</th><th scope="col">Size</th></tr></thead>
<tbody class="[&>tr>td]:px-3.5 [&>tr>td]:py-2.5 [&>tr>td]:border-b [&>tr>td]:border-tsb06-line [&>tr:last-child>td]:border-b-0 [&>tr>td:last-child]:text-tsb06-mut [&>tr>td:last-child]:tabular-nums">
<tr><td>release-notes.md</td><td>Priya Sharma</td><td>18 KB</td></tr>
<tr><td>rollback-runbook.pdf</td><td>Jordan Lee</td><td>1.2 MB</td></tr>
<tr><td>env-matrix.csv</td><td>Sam Rivera</td><td>44 KB</td></tr>
<tr><td>token-exchange.log</td><td>Ada Okafor</td><td>7.8 MB</td></tr>
<tr><td>staging-snapshot.zip</td><td>Tomas Vidal</td><td>212 MB</td></tr>
</tbody>
</table>
</main>
</div>
</section>@import "tailwindcss";
@theme {
--color-tsb06-bg: #eef1ea;
--color-tsb06-bg: oklch(94.5% 0.012 130);
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf: oklch(98% 0.008 130);
--color-tsb06-surf2: #e3e8db;
--color-tsb06-surf2: oklch(92% 0.018 130);
--color-tsb06-line: #c8d1bd;
--color-tsb06-line: oklch(84% 0.03 130);
--color-tsb06-ink: #1b2118;
--color-tsb06-ink: oklch(22% 0.02 140);
--color-tsb06-mut: #5b6653;
--color-tsb06-mut: oklch(47% 0.03 140);
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
--color-tsb06-accsoft: oklch(91% 0.03 155);
--font-tsb06-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb06-display: 'Cabin', Optima, 'Gill Sans', Candara, 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. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.tsb-06 {
width: 100%;
min-height: 100svh;
}
.tsb-06 *,
.tsb-06 *::before,
.tsb-06 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
@media (prefers-color-scheme: dark) {
.tsb-06 {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #eef1ea;
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf2: #e3e8db;
--color-tsb06-line: #c8d1bd;
--color-tsb06-ink: #1b2118;
--color-tsb06-mut: #5b6653;
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
}
}
@media (prefers-color-scheme: light) {
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
}@import "tailwindcss";
@theme {
--color-tsb06-bg: #eef1ea;
--color-tsb06-bg: oklch(94.5% 0.012 130);
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf: oklch(98% 0.008 130);
--color-tsb06-surf2: #e3e8db;
--color-tsb06-surf2: oklch(92% 0.018 130);
--color-tsb06-line: #c8d1bd;
--color-tsb06-line: oklch(84% 0.03 130);
--color-tsb06-ink: #1b2118;
--color-tsb06-ink: oklch(22% 0.02 140);
--color-tsb06-mut: #5b6653;
--color-tsb06-mut: oklch(47% 0.03 140);
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
--color-tsb06-accsoft: oklch(91% 0.03 155);
--font-tsb06-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb06-display: 'Cabin', Optima, 'Gill Sans', Candara, 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. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.tsb-06 {
width: 100%;
min-height: 100svh;
}
.tsb-06 *,
.tsb-06 *::before,
.tsb-06 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
@media (prefers-color-scheme: dark) {
.tsb-06 {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #eef1ea;
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf2: #e3e8db;
--color-tsb06-line: #c8d1bd;
--color-tsb06-ink: #1b2118;
--color-tsb06-mut: #5b6653;
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
}
}
@media (prefers-color-scheme: light) {
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
}(function () {
document.querySelectorAll('.tsb-06').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var rail = root.querySelector('#tsb-06-roving'),
items = Array.prototype.slice.call(rail.querySelectorAll('a[href]')),
index = Math.max(0, items.findIndex(function (a) { return a.hasAttribute('aria-current'); }));
function move(next) {
index = Math.min(items.length - 1, Math.max(0, next));
items.forEach(function (a, i) { a.setAttribute('tabindex', i === index ? '0' : '-1'); });
items[index].focus();
}
function onKey(e) {
var map = { ArrowDown: index + 1, ArrowUp: index - 1, Home: 0, End: items.length - 1 };
if (!(e.key in map)) return;
e.preventDefault();
move(map[e.key]);
}
function onFocus(e) { var i = items.indexOf(e.target); if (i > -1) index = i; }
rail.addEventListener('keydown', onKey);
rail.addEventListener('focusin', onFocus);
root.__tsbCleanup = function () {
rail.removeEventListener('keydown', onKey);
rail.removeEventListener('focusin', onFocus);
};
});
})();(function () {
document.querySelectorAll('.tsb-06').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var rail = root.querySelector('#tsb-06-roving'),
items = Array.prototype.slice.call(rail.querySelectorAll('a[href]')),
index = Math.max(0, items.findIndex(function (a) { return a.hasAttribute('aria-current'); }));
function move(next) {
index = Math.min(items.length - 1, Math.max(0, next));
items.forEach(function (a, i) { a.setAttribute('tabindex', i === index ? '0' : '-1'); });
items[index].focus();
}
function onKey(e) {
var map = { ArrowDown: index + 1, ArrowUp: index - 1, Home: 0, End: items.length - 1 };
if (!(e.key in map)) return;
e.preventDefault();
move(map[e.key]);
}
function onFocus(e) { var i = items.indexOf(e.target); if (i > -1) index = i; }
rail.addEventListener('keydown', onKey);
rail.addEventListener('focusin', onFocus);
root.__tsbCleanup = function () {
rail.removeEventListener('keydown', onKey);
rail.removeEventListener('focusin', onFocus);
};
});
})();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-tsb06-bg: #eef1ea;
--color-tsb06-bg: oklch(94.5% 0.012 130);
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf: oklch(98% 0.008 130);
--color-tsb06-surf2: #e3e8db;
--color-tsb06-surf2: oklch(92% 0.018 130);
--color-tsb06-line: #c8d1bd;
--color-tsb06-line: oklch(84% 0.03 130);
--color-tsb06-ink: #1b2118;
--color-tsb06-ink: oklch(22% 0.02 140);
--color-tsb06-mut: #5b6653;
--color-tsb06-mut: oklch(47% 0.03 140);
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
--color-tsb06-accsoft: oklch(91% 0.03 155);
--font-tsb06-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb06-display: 'Cabin', Optima, 'Gill Sans', Candara, 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. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.tsb-06 {
width: 100%;
min-height: 100svh;
}
.tsb-06 *,
.tsb-06 *::before,
.tsb-06 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
@media (prefers-color-scheme: dark) {
.tsb-06 {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #eef1ea;
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf2: #e3e8db;
--color-tsb06-line: #c8d1bd;
--color-tsb06-ink: #1b2118;
--color-tsb06-mut: #5b6653;
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
}
}
@media (prefers-color-scheme: light) {
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
}@import "tailwindcss";
@theme {
--color-tsb06-bg: #eef1ea;
--color-tsb06-bg: oklch(94.5% 0.012 130);
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf: oklch(98% 0.008 130);
--color-tsb06-surf2: #e3e8db;
--color-tsb06-surf2: oklch(92% 0.018 130);
--color-tsb06-line: #c8d1bd;
--color-tsb06-line: oklch(84% 0.03 130);
--color-tsb06-ink: #1b2118;
--color-tsb06-ink: oklch(22% 0.02 140);
--color-tsb06-mut: #5b6653;
--color-tsb06-mut: oklch(47% 0.03 140);
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
--color-tsb06-accsoft: oklch(91% 0.03 155);
--font-tsb06-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb06-display: 'Cabin', Optima, 'Gill Sans', Candara, 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. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.tsb-06 {
width: 100%;
min-height: 100svh;
}
.tsb-06 *,
.tsb-06 *::before,
.tsb-06 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
@media (prefers-color-scheme: dark) {
.tsb-06 {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #eef1ea;
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf2: #e3e8db;
--color-tsb06-line: #c8d1bd;
--color-tsb06-ink: #1b2118;
--color-tsb06-mut: #5b6653;
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
}
}
@media (prefers-color-scheme: light) {
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
}<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=Cabin: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=Cabin:wght@400;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Roving tabindex is the clearest case in the collection of something that CANNOT become a variant: it rewrites an attribute on ten elements every time focus moves. The port leaves it in script and spends its effort on the focus treatment instead.
focus-visible:carries two utilities here, a ring and a tint, because a roving list gives no other feedback that the arrow key did anything.- Focus styling and
aria-[current=page]:styling are kept separate, which in utilities is self-documenting: the two sets of classes sit side by side on the link and obviously mean different things. - The recessed rail is
shadow-[inset_0_2px_6px_…]— an arbitrary shadow, because an inset depth cue is a per-demo decision, not a scale value. - The link class list is identical on all ten items, which is the honest cost of utilities on a repeated element. In a component framework this is one
<li>in a map; in a copy-paste snippet it is ten lines that never drift apart. --breakpoint-r46keeps the rail's 46rem threshold rather than snapping tomd(48rem).
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 Roving Tabindex for Sidebar Keyboard Navigation
Source: https://codefronts.com/navigation/tailwind-css-sidebars/tailwind-roving-tabindex-for-sidebar-keyboard-navigation/
Ten folders means ten tab stops before a keyboard user reaches the content — unless the list is one stop and the arrow keys move inside it. Roving tabindex is script by definition (it rewrites tabindex as focus moves), so the port's interesting half is the focus treatment: focus-visible: utilities give the moved-to item a ring AND a tint, because with one tab stop the focus indicator is the only thing telling the user where they are.
## HTML
```html
<section class="tsb-06 block relative w-full min-h-svh bg-tsb06-bg text-tsb06-ink font-tsb06-sans leading-[1.55]" aria-label="Ranger file manager">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-06-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-06-theme">
<label class="grid place-items-center w-11 h-11 rounded-[0.4rem] border border-tsb06-line bg-tsb06-surf text-tsb06-ink cursor-pointer shadow-[inset_0_1px_3px_oklch(25%_0.02_140/0.14)] peer-focus-visible:outline-2 peer-focus-visible:outline-tsb06-acc peer-focus-visible:outline-offset-2" for="tsb-06-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 r46:grid-cols-[17rem_minmax(0,1fr)]">
<nav class="min-w-0 px-3 py-4 bg-tsb06-surf border-b border-tsb06-line shadow-[inset_0_2px_6px_oklch(25%_0.02_140/0.09)] r46:border-b-0 r46:border-r r46:border-tsb06-line" aria-label="Ranger folders">
<p class="m-0 mb-2 px-2.5 text-[0.72rem] tracking-[0.1em] uppercase text-tsb06-mut">All folders</p>
<ul class="list-none m-0 p-0 grid gap-px" id="tsb-06-roving">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft aria-[current=page]:bg-tsb06-accsoft aria-[current=page]:text-tsb06-acc aria-[current=page]:font-semibold" href="#tsb-06-main" tabindex="0" aria-current="page">Deployments</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Design source</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Contracts 2026</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Audit exports</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Vendor invoices</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Brand assets</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Field photos</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Board minutes</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Payroll archive</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Trash</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-06-main">
<h2 class="m-0 mb-1 font-tsb06-display text-[clamp(1.4rem,3.6vw,2rem)] leading-[1.1]">Deployments</h2>
<p class="m-0 mb-5 text-[0.88rem] text-tsb06-mut">42 items · last change 2 hours ago</p>
<table class="w-full border-collapse overflow-hidden text-[0.9rem] bg-tsb06-surf border border-tsb06-line rounded-[0.5rem]">
<thead><tr class="[&>th]:px-3.5 [&>th]:py-2.5 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb06-line [&>th]:text-[0.72rem] [&>th]:tracking-[0.1em] [&>th]:uppercase [&>th]:font-bold [&>th]:text-tsb06-mut"><th scope="col">Name</th><th scope="col">Owner</th><th scope="col">Size</th></tr></thead>
<tbody class="[&>tr>td]:px-3.5 [&>tr>td]:py-2.5 [&>tr>td]:border-b [&>tr>td]:border-tsb06-line [&>tr:last-child>td]:border-b-0 [&>tr>td:last-child]:text-tsb06-mut [&>tr>td:last-child]:tabular-nums">
<tr><td>release-notes.md</td><td>Priya Sharma</td><td>18 KB</td></tr>
<tr><td>rollback-runbook.pdf</td><td>Jordan Lee</td><td>1.2 MB</td></tr>
<tr><td>env-matrix.csv</td><td>Sam Rivera</td><td>44 KB</td></tr>
<tr><td>token-exchange.log</td><td>Ada Okafor</td><td>7.8 MB</td></tr>
<tr><td>staging-snapshot.zip</td><td>Tomas Vidal</td><td>212 MB</td></tr>
</tbody>
</table>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsb06-bg: #eef1ea;
--color-tsb06-bg: oklch(94.5% 0.012 130);
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf: oklch(98% 0.008 130);
--color-tsb06-surf2: #e3e8db;
--color-tsb06-surf2: oklch(92% 0.018 130);
--color-tsb06-line: #c8d1bd;
--color-tsb06-line: oklch(84% 0.03 130);
--color-tsb06-ink: #1b2118;
--color-tsb06-ink: oklch(22% 0.02 140);
--color-tsb06-mut: #5b6653;
--color-tsb06-mut: oklch(47% 0.03 140);
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
--color-tsb06-accsoft: oklch(91% 0.03 155);
--font-tsb06-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb06-display: 'Cabin', Optima, 'Gill Sans', Candara, 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. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.tsb-06 {
width: 100%;
min-height: 100svh;
}
.tsb-06 *,
.tsb-06 *::before,
.tsb-06 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
@media (prefers-color-scheme: dark) {
.tsb-06 {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #eef1ea;
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf2: #e3e8db;
--color-tsb06-line: #c8d1bd;
--color-tsb06-ink: #1b2118;
--color-tsb06-mut: #5b6653;
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
}
}
@media (prefers-color-scheme: light) {
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.tsb-06').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var rail = root.querySelector('#tsb-06-roving'),
items = Array.prototype.slice.call(rail.querySelectorAll('a[href]')),
index = Math.max(0, items.findIndex(function (a) { return a.hasAttribute('aria-current'); }));
function move(next) {
index = Math.min(items.length - 1, Math.max(0, next));
items.forEach(function (a, i) { a.setAttribute('tabindex', i === index ? '0' : '-1'); });
items[index].focus();
}
function onKey(e) {
var map = { ArrowDown: index + 1, ArrowUp: index - 1, Home: 0, End: items.length - 1 };
if (!(e.key in map)) return;
e.preventDefault();
move(map[e.key]);
}
function onFocus(e) { var i = items.indexOf(e.target); if (i > -1) index = i; }
rail.addEventListener('keydown', onKey);
rail.addEventListener('focusin', onFocus);
root.__tsbCleanup = function () {
rail.removeEventListener('keydown', onKey);
rail.removeEventListener('focusin', onFocus);
};
});
})();
```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 Roving Tabindex for Sidebar Keyboard Navigation
Source: https://codefronts.com/navigation/tailwind-css-sidebars/tailwind-roving-tabindex-for-sidebar-keyboard-navigation/
Ten folders means ten tab stops before a keyboard user reaches the content — unless the list is one stop and the arrow keys move inside it. Roving tabindex is script by definition (it rewrites tabindex as focus moves), so the port's interesting half is the focus treatment: focus-visible: utilities give the moved-to item a ring AND a tint, because with one tab stop the focus indicator is the only thing telling the user where they are.
## HTML
```html
<section class="tsb-06 block relative w-full min-h-svh bg-tsb06-bg text-tsb06-ink font-tsb06-sans leading-[1.55]" aria-label="Ranger file manager">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-06-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-06-theme">
<label class="grid place-items-center w-11 h-11 rounded-[0.4rem] border border-tsb06-line bg-tsb06-surf text-tsb06-ink cursor-pointer shadow-[inset_0_1px_3px_oklch(25%_0.02_140/0.14)] peer-focus-visible:outline-2 peer-focus-visible:outline-tsb06-acc peer-focus-visible:outline-offset-2" for="tsb-06-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 r46:grid-cols-[17rem_minmax(0,1fr)]">
<nav class="min-w-0 px-3 py-4 bg-tsb06-surf border-b border-tsb06-line shadow-[inset_0_2px_6px_oklch(25%_0.02_140/0.09)] r46:border-b-0 r46:border-r r46:border-tsb06-line" aria-label="Ranger folders">
<p class="m-0 mb-2 px-2.5 text-[0.72rem] tracking-[0.1em] uppercase text-tsb06-mut">All folders</p>
<ul class="list-none m-0 p-0 grid gap-px" id="tsb-06-roving">
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft aria-[current=page]:bg-tsb06-accsoft aria-[current=page]:text-tsb06-acc aria-[current=page]:font-semibold" href="#tsb-06-main" tabindex="0" aria-current="page">Deployments</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Design source</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Contracts 2026</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Audit exports</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Vendor invoices</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Brand assets</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Field photos</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Board minutes</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Payroll archive</a></li>
<li><a class="flex items-center min-w-0 min-h-11 px-2.5 py-2 rounded-[0.3rem] text-[0.93rem] text-tsb06-ink no-underline hover:bg-tsb06-surf2 focus-visible:outline-2 focus-visible:outline-tsb06-acc focus-visible:-outline-offset-2 focus-visible:bg-tsb06-accsoft" href="#tsb-06-main" tabindex="-1">Trash</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-06-main">
<h2 class="m-0 mb-1 font-tsb06-display text-[clamp(1.4rem,3.6vw,2rem)] leading-[1.1]">Deployments</h2>
<p class="m-0 mb-5 text-[0.88rem] text-tsb06-mut">42 items · last change 2 hours ago</p>
<table class="w-full border-collapse overflow-hidden text-[0.9rem] bg-tsb06-surf border border-tsb06-line rounded-[0.5rem]">
<thead><tr class="[&>th]:px-3.5 [&>th]:py-2.5 [&>th]:text-start [&>th]:border-b [&>th]:border-tsb06-line [&>th]:text-[0.72rem] [&>th]:tracking-[0.1em] [&>th]:uppercase [&>th]:font-bold [&>th]:text-tsb06-mut"><th scope="col">Name</th><th scope="col">Owner</th><th scope="col">Size</th></tr></thead>
<tbody class="[&>tr>td]:px-3.5 [&>tr>td]:py-2.5 [&>tr>td]:border-b [&>tr>td]:border-tsb06-line [&>tr:last-child>td]:border-b-0 [&>tr>td:last-child]:text-tsb06-mut [&>tr>td:last-child]:tabular-nums">
<tr><td>release-notes.md</td><td>Priya Sharma</td><td>18 KB</td></tr>
<tr><td>rollback-runbook.pdf</td><td>Jordan Lee</td><td>1.2 MB</td></tr>
<tr><td>env-matrix.csv</td><td>Sam Rivera</td><td>44 KB</td></tr>
<tr><td>token-exchange.log</td><td>Ada Okafor</td><td>7.8 MB</td></tr>
<tr><td>staging-snapshot.zip</td><td>Tomas Vidal</td><td>212 MB</td></tr>
</tbody>
</table>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsb06-bg: #eef1ea;
--color-tsb06-bg: oklch(94.5% 0.012 130);
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf: oklch(98% 0.008 130);
--color-tsb06-surf2: #e3e8db;
--color-tsb06-surf2: oklch(92% 0.018 130);
--color-tsb06-line: #c8d1bd;
--color-tsb06-line: oklch(84% 0.03 130);
--color-tsb06-ink: #1b2118;
--color-tsb06-ink: oklch(22% 0.02 140);
--color-tsb06-mut: #5b6653;
--color-tsb06-mut: oklch(47% 0.03 140);
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
--color-tsb06-accsoft: oklch(91% 0.03 155);
--font-tsb06-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb06-display: 'Cabin', Optima, 'Gill Sans', Candara, 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. It scopes the demo, and it holds the stage at full height in the
moment before the browser build has compiled its utilities. */
.tsb-06 {
width: 100%;
min-height: 100svh;
}
.tsb-06 *,
.tsb-06 *::before,
.tsb-06 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
@media (prefers-color-scheme: dark) {
.tsb-06 {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #eef1ea;
--color-tsb06-surf: #f8faf5;
--color-tsb06-surf2: #e3e8db;
--color-tsb06-line: #c8d1bd;
--color-tsb06-ink: #1b2118;
--color-tsb06-mut: #5b6653;
--color-tsb06-acc: #2f6b45;
--color-tsb06-acc: oklch(46% 0.1 155);
--color-tsb06-accsoft: #d9e8dd;
}
}
@media (prefers-color-scheme: light) {
.tsb-06:has(.tsb-06-theme-input:checked) {
--color-tsb06-bg: #0f1310;
--color-tsb06-surf: #161c16;
--color-tsb06-surf2: #1e251d;
--color-tsb06-line: #2d372b;
--color-tsb06-ink: #eaf0e6;
--color-tsb06-mut: #9aa694;
--color-tsb06-acc: #8fd2a8;
--color-tsb06-acc: oklch(82% 0.11 155);
--color-tsb06-accsoft: #1d2c22;
}
}
```
## JavaScript
```js
(function () {
document.querySelectorAll('.tsb-06').forEach(function (root) {
if (root.dataset.bound) return; root.dataset.bound = '1';
var rail = root.querySelector('#tsb-06-roving'),
items = Array.prototype.slice.call(rail.querySelectorAll('a[href]')),
index = Math.max(0, items.findIndex(function (a) { return a.hasAttribute('aria-current'); }));
function move(next) {
index = Math.min(items.length - 1, Math.max(0, next));
items.forEach(function (a, i) { a.setAttribute('tabindex', i === index ? '0' : '-1'); });
items[index].focus();
}
function onKey(e) {
var map = { ArrowDown: index + 1, ArrowUp: index - 1, Home: 0, End: items.length - 1 };
if (!(e.key in map)) return;
e.preventDefault();
move(map[e.key]);
}
function onFocus(e) { var i = items.indexOf(e.target); if (i > -1) index = i; }
rail.addEventListener('keydown', onKey);
rail.addEventListener('focusin', onFocus);
root.__tsbCleanup = function () {
rail.removeEventListener('keydown', onKey);
rail.removeEventListener('focusin', onFocus);
};
});
})();
```How this works
The rule the pattern exists for: one composite widget, one tab stop. Exactly one link in the rail has tabindex="0"; the rest carry -1. ↓/↑ move, Home/End jump, and the script moves the 0 with focus so the rail remembers where you were when you tab back into it.
Nothing about that is expressible as a variant, and the port does not pretend otherwise. What the class list contributes is the part that makes it usable:
class="… focus-visible:outline-2 focus-visible:outline-tsb06-acc
focus-visible:-outline-offset-2
focus-visible:bg-tsb06-accsoft"With ten items sharing one tab stop, arrowing down changes nothing visible except the focus indicator — so it gets a tint as well as a ring.
Focus and current are deliberately separate styles. aria-current="page" says which folder you are looking at; focus says which one you are about to open. Merging them into one treatment is the most common way this pattern gets confusing.
The inner focus offset matters here. -outline-offset-2 keeps the ring inside the item's own box, so in a 17rem rail it never clips against the border.
Make it yours
- Drop the Home/End entries from the key map and the pattern still works — arrows are the required part.
- Add type-ahead by matching
e.keyagainst the first letter of each item's text; the roving index is already the state it needs. - Switch the tint on focus off by deleting one utility if your ring is strong enough on its own.
- Retheme through
--color-tsb06-acc; the ring, the tint and the active item all read it. - Change the rail width in
r46:grid-cols-[17rem_minmax(0,1fr)]. - If the list is short (four or five items) do NOT use this pattern — plain tab stops are better, and demo 01 is the build.
Gotchas — read before shipping
- Roving tabindex on a list of only a few links makes navigation worse, not better. It earns its complexity somewhere around eight items.
- Every item must be reachable by arrow keys AND the active one must keep
tabindex="0"when focus leaves, or tabbing back in lands on the first item every time. focus-visible:alone is not enough feedback in a roving list; without the tint, arrowing feels like nothing happened on low-contrast displays.- Do not add
role="tablist"because you added arrow keys. It is still navigation — demo 04 is the boundary marker. e.preventDefault()on the arrow keys is required, or the page scrolls under the moving focus.- The script must re-read the focused index on
focusin: a mouse click can move focus without touching the key handler.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Tailwind v4's generated sheet sets the floor. :focus-visible is Baseline 2022 and the roving pattern is plain keydown handling with no floor at all.