13 / 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
Pure Tailwind Icon Rail Sidebar with Accessible Labels
An icon-only rail has one hard requirement and one nice-to-have, and most implementations get them backwards. The requirement is an accessible name on every link — here a visually hidden <span>, never title. The nice-to-have is the sighted user's tooltip, and in utilities it is group-hover: and group-focus-visible: on a second span. Zero script.
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-13 block relative w-full min-h-svh bg-tsb13-bg text-tsb13-ink font-tsb13-sans leading-[1.55]" aria-label="Aperture media library">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-13-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-13-theme">
<label class="grid place-items-center w-11 h-11 border border-tsb13-line bg-tsb13-surf text-tsb13-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb13-acc peer-focus-visible:outline-offset-2" for="tsb-13-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="flex min-w-0 min-h-svh bg-tsb13-surf">
<nav class="flex-none min-w-0 w-[var(--tsb13-w)] bg-tsb13-surf2 border-r border-tsb13-line" aria-label="Aperture library">
<ul class="list-none m-0 p-1 grid gap-1">
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb13-accsoft aria-[current=page]:text-tsb13-acc aria-[current=page]:shadow-[inset_3px_0_0_var(--color-tsb13-acc)]" href="#tsb-13-main" aria-current="page">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4" y="5" width="16" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M4 15l4-4 4 4 3-3 5 5" fill="none" stroke="currentColor" stroke-width="1.7"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">All media</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">All media</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h6l2 2h8v9H4Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Collections</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Collections</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M15.5 15.5L20 20" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Saved searches</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Saved searches</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4l7 4v8l-7 4-7-4V8Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Renditions</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Renditions</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 19V5h9l5 5v9Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M9 13h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Licences</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Licences</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 4h12v16l-6-4-6 4Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Archive</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Archive</span></a></li>
</ul>
</nav>
<main class="flex-1 min-w-0 grid gap-4.5 content-start px-5 pt-5.5 pb-10 r46:px-8 r46:pt-9 r46:pb-12" id="tsb-13-main">
<div class="min-w-0">
<h2 class="m-0 mb-1 font-tsb13-display text-[clamp(1.5rem,3.6vw,2rem)] tracking-[0.02em] uppercase">All media</h2>
<p class="m-0 text-[0.84rem] tabular-nums text-tsb13-mut">1,842 assets · 41 GB · last ingest 14:02</p>
</div>
<div class="grid grid-cols-[repeat(auto-fill,minmax(7rem,1fr))] gap-2 [&>span]:aspect-[4/3] [&>span]:bg-tsb13-surf2 [&>span]:border [&>span]:border-tsb13-line [&>span:nth-child(3n)]:bg-tsb13-accsoft" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</main>
</div>
</section><section class="tsb-13 block relative w-full min-h-svh bg-tsb13-bg text-tsb13-ink font-tsb13-sans leading-[1.55]" aria-label="Aperture media library">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-13-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-13-theme">
<label class="grid place-items-center w-11 h-11 border border-tsb13-line bg-tsb13-surf text-tsb13-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb13-acc peer-focus-visible:outline-offset-2" for="tsb-13-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="flex min-w-0 min-h-svh bg-tsb13-surf">
<nav class="flex-none min-w-0 w-[var(--tsb13-w)] bg-tsb13-surf2 border-r border-tsb13-line" aria-label="Aperture library">
<ul class="list-none m-0 p-1 grid gap-1">
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb13-accsoft aria-[current=page]:text-tsb13-acc aria-[current=page]:shadow-[inset_3px_0_0_var(--color-tsb13-acc)]" href="#tsb-13-main" aria-current="page">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4" y="5" width="16" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M4 15l4-4 4 4 3-3 5 5" fill="none" stroke="currentColor" stroke-width="1.7"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">All media</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">All media</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h6l2 2h8v9H4Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Collections</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Collections</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M15.5 15.5L20 20" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Saved searches</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Saved searches</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4l7 4v8l-7 4-7-4V8Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Renditions</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Renditions</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 19V5h9l5 5v9Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M9 13h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Licences</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Licences</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 4h12v16l-6-4-6 4Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Archive</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Archive</span></a></li>
</ul>
</nav>
<main class="flex-1 min-w-0 grid gap-4.5 content-start px-5 pt-5.5 pb-10 r46:px-8 r46:pt-9 r46:pb-12" id="tsb-13-main">
<div class="min-w-0">
<h2 class="m-0 mb-1 font-tsb13-display text-[clamp(1.5rem,3.6vw,2rem)] tracking-[0.02em] uppercase">All media</h2>
<p class="m-0 text-[0.84rem] tabular-nums text-tsb13-mut">1,842 assets · 41 GB · last ingest 14:02</p>
</div>
<div class="grid grid-cols-[repeat(auto-fill,minmax(7rem,1fr))] gap-2 [&>span]:aspect-[4/3] [&>span]:bg-tsb13-surf2 [&>span]:border [&>span]:border-tsb13-line [&>span:nth-child(3n)]:bg-tsb13-accsoft" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</main>
</div>
</section>@import "tailwindcss";
@theme {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-bg: oklch(96% 0.002 285);
--color-tsb13-surf: #ffffff;
--color-tsb13-surf: oklch(100% 0 0);
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-surf2: oklch(92.5% 0.004 285);
--color-tsb13-line: #d3d3d8;
--color-tsb13-line: oklch(86.5% 0.006 285);
--color-tsb13-ink: #131316;
--color-tsb13-ink: oklch(19% 0.004 285);
--color-tsb13-mut: #5c5c66;
--color-tsb13-mut: oklch(46% 0.012 285);
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
--color-tsb13-accsoft: oklch(92% 0.03 232);
--font-tsb13-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb13-display: 'Roboto Condensed', 'Archivo Narrow', ui-sans-serif, system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the rail width, which is geometry read by
w-[var(--tsb13-w)] rather than a colour scale. */
.tsb-13 {
width: 100%;
min-height: 100svh;
--tsb13-w: 4rem;
}
.tsb-13 *,
.tsb-13 *::before,
.tsb-13 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
@media (prefers-color-scheme: dark) {
.tsb-13 {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-surf: #ffffff;
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-line: #d3d3d8;
--color-tsb13-ink: #131316;
--color-tsb13-mut: #5c5c66;
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
}
}
@media (prefers-color-scheme: light) {
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
}@import "tailwindcss";
@theme {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-bg: oklch(96% 0.002 285);
--color-tsb13-surf: #ffffff;
--color-tsb13-surf: oklch(100% 0 0);
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-surf2: oklch(92.5% 0.004 285);
--color-tsb13-line: #d3d3d8;
--color-tsb13-line: oklch(86.5% 0.006 285);
--color-tsb13-ink: #131316;
--color-tsb13-ink: oklch(19% 0.004 285);
--color-tsb13-mut: #5c5c66;
--color-tsb13-mut: oklch(46% 0.012 285);
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
--color-tsb13-accsoft: oklch(92% 0.03 232);
--font-tsb13-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb13-display: 'Roboto Condensed', 'Archivo Narrow', ui-sans-serif, system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the rail width, which is geometry read by
w-[var(--tsb13-w)] rather than a colour scale. */
.tsb-13 {
width: 100%;
min-height: 100svh;
--tsb13-w: 4rem;
}
.tsb-13 *,
.tsb-13 *::before,
.tsb-13 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
@media (prefers-color-scheme: dark) {
.tsb-13 {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-surf: #ffffff;
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-line: #d3d3d8;
--color-tsb13-ink: #131316;
--color-tsb13-mut: #5c5c66;
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
}
}
@media (prefers-color-scheme: light) {
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
}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-tsb13-bg: #f4f4f5;
--color-tsb13-bg: oklch(96% 0.002 285);
--color-tsb13-surf: #ffffff;
--color-tsb13-surf: oklch(100% 0 0);
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-surf2: oklch(92.5% 0.004 285);
--color-tsb13-line: #d3d3d8;
--color-tsb13-line: oklch(86.5% 0.006 285);
--color-tsb13-ink: #131316;
--color-tsb13-ink: oklch(19% 0.004 285);
--color-tsb13-mut: #5c5c66;
--color-tsb13-mut: oklch(46% 0.012 285);
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
--color-tsb13-accsoft: oklch(92% 0.03 232);
--font-tsb13-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb13-display: 'Roboto Condensed', 'Archivo Narrow', ui-sans-serif, system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the rail width, which is geometry read by
w-[var(--tsb13-w)] rather than a colour scale. */
.tsb-13 {
width: 100%;
min-height: 100svh;
--tsb13-w: 4rem;
}
.tsb-13 *,
.tsb-13 *::before,
.tsb-13 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
@media (prefers-color-scheme: dark) {
.tsb-13 {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-surf: #ffffff;
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-line: #d3d3d8;
--color-tsb13-ink: #131316;
--color-tsb13-mut: #5c5c66;
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
}
}
@media (prefers-color-scheme: light) {
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
}@import "tailwindcss";
@theme {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-bg: oklch(96% 0.002 285);
--color-tsb13-surf: #ffffff;
--color-tsb13-surf: oklch(100% 0 0);
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-surf2: oklch(92.5% 0.004 285);
--color-tsb13-line: #d3d3d8;
--color-tsb13-line: oklch(86.5% 0.006 285);
--color-tsb13-ink: #131316;
--color-tsb13-ink: oklch(19% 0.004 285);
--color-tsb13-mut: #5c5c66;
--color-tsb13-mut: oklch(46% 0.012 285);
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
--color-tsb13-accsoft: oklch(92% 0.03 232);
--font-tsb13-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb13-display: 'Roboto Condensed', 'Archivo Narrow', ui-sans-serif, system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the rail width, which is geometry read by
w-[var(--tsb13-w)] rather than a colour scale. */
.tsb-13 {
width: 100%;
min-height: 100svh;
--tsb13-w: 4rem;
}
.tsb-13 *,
.tsb-13 *::before,
.tsb-13 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
@media (prefers-color-scheme: dark) {
.tsb-13 {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-surf: #ffffff;
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-line: #d3d3d8;
--color-tsb13-ink: #131316;
--color-tsb13-mut: #5c5c66;
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
}
}
@media (prefers-color-scheme: light) {
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
}<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=Roboto+Condensed: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=Roboto+Condensed:wght@400;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The tooltip is the textbook
groupcase:groupon the link,group-hover:andgroup-focus-visible:on the span. The CSS version needs two selectors written elsewhere; here the trigger and the response are one element apart. invisible opacity-0resting, both flipped in the variants —opacity-0alone would leave a hit-testable, sometimes-announced element floating beside the rail.transition-[opacity,visibility]is a composed transition with no utility form on the scale, so it is written as an arbitrary property list — the same technique as demo 02's delayed visibility.[&>svg]:w-5.5keeps icon sizing on the link, which matters here because the link has two span children that must NOT be sized like icons.[&>span:nth-child(3n)]:bg-tsb13-accsofton the asset grid is a genuinenth-childport: Tailwind'snth-*variants need static values, and as a child selector on the parent it stays one utility for eighteen tiles.- Zero JS, and the rail is 4rem — the whole demo is the argument that icon-only rails are an accessibility problem, not a layout problem.
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: Pure Tailwind Icon Rail Sidebar with Accessible Labels
Source: https://codefronts.com/navigation/tailwind-css-sidebars/pure-tailwind-icon-rail-sidebar-with-accessible-labels/
An icon-only rail has one hard requirement and one nice-to-have, and most implementations get them backwards. The requirement is an accessible name on every link — here a visually hidden <span>, never title. The nice-to-have is the sighted user's tooltip, and in utilities it is group-hover: and group-focus-visible: on a second span. Zero script.
## HTML
```html
<section class="tsb-13 block relative w-full min-h-svh bg-tsb13-bg text-tsb13-ink font-tsb13-sans leading-[1.55]" aria-label="Aperture media library">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-13-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-13-theme">
<label class="grid place-items-center w-11 h-11 border border-tsb13-line bg-tsb13-surf text-tsb13-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb13-acc peer-focus-visible:outline-offset-2" for="tsb-13-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="flex min-w-0 min-h-svh bg-tsb13-surf">
<nav class="flex-none min-w-0 w-[var(--tsb13-w)] bg-tsb13-surf2 border-r border-tsb13-line" aria-label="Aperture library">
<ul class="list-none m-0 p-1 grid gap-1">
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb13-accsoft aria-[current=page]:text-tsb13-acc aria-[current=page]:shadow-[inset_3px_0_0_var(--color-tsb13-acc)]" href="#tsb-13-main" aria-current="page">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4" y="5" width="16" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M4 15l4-4 4 4 3-3 5 5" fill="none" stroke="currentColor" stroke-width="1.7"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">All media</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">All media</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h6l2 2h8v9H4Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Collections</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Collections</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M15.5 15.5L20 20" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Saved searches</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Saved searches</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4l7 4v8l-7 4-7-4V8Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Renditions</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Renditions</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 19V5h9l5 5v9Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M9 13h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Licences</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Licences</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 4h12v16l-6-4-6 4Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Archive</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Archive</span></a></li>
</ul>
</nav>
<main class="flex-1 min-w-0 grid gap-4.5 content-start px-5 pt-5.5 pb-10 r46:px-8 r46:pt-9 r46:pb-12" id="tsb-13-main">
<div class="min-w-0">
<h2 class="m-0 mb-1 font-tsb13-display text-[clamp(1.5rem,3.6vw,2rem)] tracking-[0.02em] uppercase">All media</h2>
<p class="m-0 text-[0.84rem] tabular-nums text-tsb13-mut">1,842 assets · 41 GB · last ingest 14:02</p>
</div>
<div class="grid grid-cols-[repeat(auto-fill,minmax(7rem,1fr))] gap-2 [&>span]:aspect-[4/3] [&>span]:bg-tsb13-surf2 [&>span]:border [&>span]:border-tsb13-line [&>span:nth-child(3n)]:bg-tsb13-accsoft" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-bg: oklch(96% 0.002 285);
--color-tsb13-surf: #ffffff;
--color-tsb13-surf: oklch(100% 0 0);
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-surf2: oklch(92.5% 0.004 285);
--color-tsb13-line: #d3d3d8;
--color-tsb13-line: oklch(86.5% 0.006 285);
--color-tsb13-ink: #131316;
--color-tsb13-ink: oklch(19% 0.004 285);
--color-tsb13-mut: #5c5c66;
--color-tsb13-mut: oklch(46% 0.012 285);
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
--color-tsb13-accsoft: oklch(92% 0.03 232);
--font-tsb13-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb13-display: 'Roboto Condensed', 'Archivo Narrow', ui-sans-serif, system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the rail width, which is geometry read by
w-[var(--tsb13-w)] rather than a colour scale. */
.tsb-13 {
width: 100%;
min-height: 100svh;
--tsb13-w: 4rem;
}
.tsb-13 *,
.tsb-13 *::before,
.tsb-13 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
@media (prefers-color-scheme: dark) {
.tsb-13 {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-surf: #ffffff;
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-line: #d3d3d8;
--color-tsb13-ink: #131316;
--color-tsb13-mut: #5c5c66;
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
}
}
@media (prefers-color-scheme: light) {
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
}
```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: Pure Tailwind Icon Rail Sidebar with Accessible Labels
Source: https://codefronts.com/navigation/tailwind-css-sidebars/pure-tailwind-icon-rail-sidebar-with-accessible-labels/
An icon-only rail has one hard requirement and one nice-to-have, and most implementations get them backwards. The requirement is an accessible name on every link — here a visually hidden <span>, never title. The nice-to-have is the sighted user's tooltip, and in utilities it is group-hover: and group-focus-visible: on a second span. Zero script.
## HTML
```html
<section class="tsb-13 block relative w-full min-h-svh bg-tsb13-bg text-tsb13-ink font-tsb13-sans leading-[1.55]" aria-label="Aperture media library">
<div class="absolute top-3.5 right-3.5 z-60">
<input class="tsb-13-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-13-theme">
<label class="grid place-items-center w-11 h-11 border border-tsb13-line bg-tsb13-surf text-tsb13-ink cursor-pointer peer-focus-visible:outline-2 peer-focus-visible:outline-tsb13-acc peer-focus-visible:outline-offset-2" for="tsb-13-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="flex min-w-0 min-h-svh bg-tsb13-surf">
<nav class="flex-none min-w-0 w-[var(--tsb13-w)] bg-tsb13-surf2 border-r border-tsb13-line" aria-label="Aperture library">
<ul class="list-none m-0 p-1 grid gap-1">
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2 aria-[current=page]:bg-tsb13-accsoft aria-[current=page]:text-tsb13-acc aria-[current=page]:shadow-[inset_3px_0_0_var(--color-tsb13-acc)]" href="#tsb-13-main" aria-current="page">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4" y="5" width="16" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M4 15l4-4 4 4 3-3 5 5" fill="none" stroke="currentColor" stroke-width="1.7"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">All media</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">All media</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h6l2 2h8v9H4Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Collections</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Collections</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M15.5 15.5L20 20" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Saved searches</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Saved searches</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4l7 4v8l-7 4-7-4V8Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Renditions</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Renditions</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 19V5h9l5 5v9Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M9 13h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Licences</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Licences</span></a></li>
<li><a class="group relative grid place-items-center h-11 min-h-11 text-tsb13-mut no-underline [&>svg]:w-5.5 [&>svg]:h-5.5 hover:bg-tsb13-surf hover:text-tsb13-ink focus-visible:outline-2 focus-visible:outline-tsb13-acc focus-visible:-outline-offset-2" href="#tsb-13-main">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 4h12v16l-6-4-6 4Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>
<span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Archive</span><span class="absolute left-[calc(100%+0.55rem)] top-1/2 -translate-y-1/2 z-8 px-2 py-1 whitespace-nowrap pointer-events-none invisible opacity-0 bg-tsb13-ink text-tsb13-bg font-tsb13-display text-[0.8rem] tracking-[0.04em] uppercase transition-[opacity,visibility] duration-[120ms] group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100 motion-reduce:transition-none" aria-hidden="true">Archive</span></a></li>
</ul>
</nav>
<main class="flex-1 min-w-0 grid gap-4.5 content-start px-5 pt-5.5 pb-10 r46:px-8 r46:pt-9 r46:pb-12" id="tsb-13-main">
<div class="min-w-0">
<h2 class="m-0 mb-1 font-tsb13-display text-[clamp(1.5rem,3.6vw,2rem)] tracking-[0.02em] uppercase">All media</h2>
<p class="m-0 text-[0.84rem] tabular-nums text-tsb13-mut">1,842 assets · 41 GB · last ingest 14:02</p>
</div>
<div class="grid grid-cols-[repeat(auto-fill,minmax(7rem,1fr))] gap-2 [&>span]:aspect-[4/3] [&>span]:bg-tsb13-surf2 [&>span]:border [&>span]:border-tsb13-line [&>span:nth-child(3n)]:bg-tsb13-accsoft" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
<span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</main>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-bg: oklch(96% 0.002 285);
--color-tsb13-surf: #ffffff;
--color-tsb13-surf: oklch(100% 0 0);
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-surf2: oklch(92.5% 0.004 285);
--color-tsb13-line: #d3d3d8;
--color-tsb13-line: oklch(86.5% 0.006 285);
--color-tsb13-ink: #131316;
--color-tsb13-ink: oklch(19% 0.004 285);
--color-tsb13-mut: #5c5c66;
--color-tsb13-mut: oklch(46% 0.012 285);
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
--color-tsb13-accsoft: oklch(92% 0.03 232);
--font-tsb13-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
--font-tsb13-display: 'Roboto Condensed', 'Archivo Narrow', ui-sans-serif, system-ui, sans-serif;
--breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract plus the rail width, which is geometry read by
w-[var(--tsb13-w)] rather than a colour scale. */
.tsb-13 {
width: 100%;
min-height: 100svh;
--tsb13-w: 4rem;
}
.tsb-13 *,
.tsb-13 *::before,
.tsb-13 *::after {
box-sizing: border-box;
}
/* Token flip for the pure-CSS theme toggle, plus the two mandatory mirror blocks. */
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
@media (prefers-color-scheme: dark) {
.tsb-13 {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #f4f4f5;
--color-tsb13-surf: #ffffff;
--color-tsb13-surf2: #e9e9ec;
--color-tsb13-line: #d3d3d8;
--color-tsb13-ink: #131316;
--color-tsb13-mut: #5c5c66;
--color-tsb13-acc: #0f5c8c;
--color-tsb13-acc: oklch(45% 0.1 232);
--color-tsb13-accsoft: #dceaf4;
}
}
@media (prefers-color-scheme: light) {
.tsb-13:has(.tsb-13-theme-input:checked) {
--color-tsb13-bg: #0d0d0f;
--color-tsb13-surf: #15151a;
--color-tsb13-surf2: #1d1d23;
--color-tsb13-line: #2a2a31;
--color-tsb13-ink: #eeeef1;
--color-tsb13-mut: #9a9aa4;
--color-tsb13-acc: #6fbde8;
--color-tsb13-acc: oklch(78% 0.1 232);
--color-tsb13-accsoft: #16283a;
}
}
```How this works
Two labels, two audiences, one link. The visually hidden span is the accessible name: it is in the accessibility tree, it is what a screen reader announces, and it is not affected by hover at all. The tooltip is decoration for sighted users and is aria-hidden, so the name is never announced twice.
<a class="group relative grid place-items-center h-11">
<svg …>
<span class="absolute w-px h-px … [clip-path:inset(50%)]">Collections</span>
<span aria-hidden="true" class="invisible opacity-0
group-hover:visible group-hover:opacity-100
group-focus-visible:visible group-focus-visible:opacity-100">Collections</span>Why not title. It never appears on touch, it is announced inconsistently, it cannot be styled, and its delay is not configurable. It is the single most common shortcut in icon rails and it fails the users it is supposed to help.
The keyboard twin is not optional. A tooltip that only responds to hover: leaves a keyboard user tabbing through six unlabelled squares. group-focus-visible: costs two utilities.
invisible rather than opacity-0. A zero-opacity tooltip still occupies space for hit-testing and can be picked up by some assistive tooling. invisible plus the opacity transition gives the fade AND the correct resting state.
Make it yours
- Rail width is
--tsb13-win the root rule, read byw-[var(--tsb13-w)]; do not go below 44px of target. - Flip the tooltip to the other side by swapping
left-[calc(100%+0.55rem)]forright-[calc(100%+0.55rem)]. - Slow the fade with the
duration-[120ms]on the tooltip, or delete the transition for an instant label. - Retheme through
--color-tsb13-accand--color-tsb13-accsoft. - Add a text label under each icon instead of a flyout if the rail can afford 5rem — then delete the tooltip span entirely and keep the visually hidden one.
- Need the rail to expand rather than tooltip? That is demo 05 (scripted) or demo 11 (checkbox).
Gotchas — read before shipping
titleis not an accessible name substitute. It is the shortcut this demo exists to replace.- A hover-only tooltip excludes keyboard users; the
group-focus-visible:twin is required, not a nicety. - The tooltip must be
aria-hiddenAND the real label visually hidden — without the first, the name is announced twice. pointer-events-noneon the tooltip prevents it from stealing the hover and flickering on the boundary.- An icon target under 44px fails SC 2.5.8 whatever the rail width;
h-11 min-h-11is the floor. - CSS tooltips cannot escape an ancestor with
overflow: hidden. If the rail is clipped, the tooltip is clipped — use a popover or an expanding rail instead.
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 clip-path: inset(50%) Baseline 2021; the visually hidden label itself has no version floor worth quoting.