30 Tailwind CSS Sidebar Layouts
14 / 30

All 30 in 30 Tailwind CSS Sidebar Layouts

  1. 01Tailwind SaaS Dashboard Sidebar with Native Dark Mode Switch
  2. 02Tailwind Collapsible Admin Sidebar with Icon Tooltips
  3. 03Tailwind Documentation Sidebar with Multi-Level :target Tree
  4. 04Tailwind Responsive Sidebar to Bottom Navigation Switch Layout
  5. 05Tailwind Analytics Sidebar with CSS Subgrid Column Alignment
  6. 06Tailwind Animated Collapsible Sidebar with Icon-Only Mode
  7. 07Tailwind Sticky Sidebar Layout with Scroll-Driven Progress Bar
  8. 08Tailwind Pure CSS Collapsible Sidebar Drawer without JavaScript
  9. 09Tailwind Fixed Sidebar Layout with Independently Scrolling Content
  10. 10Tailwind Off-Canvas Navigation Drawer with the Popover API
  11. 11Tailwind Right-Aligned Sidebar Layout for Inspector Panels
  12. 12Tailwind Two-Pane Webmail Layout with List and Reading Pane
  13. 13Tailwind Dual Sidebar App Shell with Left Nav and Context Panel
  14. 14Tailwind Mini Icon Rail Sidebar with Anchor-Positioned Active Pill
  15. 15Tailwind E-Commerce Filter Sidebar with Native Details Accordion
  16. 16Tailwind Floating Glassmorphism Sidebar with Gradient Hairline
  17. 17Tailwind Sidebar Menu with Pinned User Profile Footer
  18. 18Tailwind Push-Content Sidebar Drawer with GPU Transitions
  19. 19Tailwind Off-Canvas Cart Overlay with Blurred Backdrop Scrim
  20. 20Tailwind Sidebar with Persistent Collapsed State in localStorage
  21. 21Tailwind Sidebar with Sticky Search Bar Inside the Rail
  22. 22Tailwind Section-Grouped Sidebar Navigation with Header Labels
  23. 23Tailwind Sliding Active Indicator Sidebar with CSS :target
  24. 24Tailwind Multi-Level Nested Tree Sidebar with Details Branches
  25. 25Tailwind Tabbed Sidebar with Radio-Driven Panel Switching
  26. 26Tailwind Onboarding Wizard Sidebar with Step Progress Tracking
  27. 27Tailwind Resizable Sidebar Layout Using the CSS Resize Property
  28. 28Tailwind Three-Pane Workspace Shell with Icon Rail and List
  29. 29Tailwind Adaptive Sidebar Layout Using CSS Container Queries
  30. 30Tailwind Frosted Glass Floating Sidebar with Animated Aurora
Pure TailwindMIT licensed

Tailwind Mini Icon Rail Sidebar with Anchor-Positioned Active Pill

The 68px icon rail perfected: 44px targets, hover tooltips, notification dots, a bottom-pinned avatar — and an active pill that glides between icons using CSS anchor positioning. The effect teams reach for a motion library to get, with no script and a clean fallback.

Live Demo
Plain CSSTailwind v4
Try it

Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69

Cruip · Mosaic by Cruip · Affiliate link

View Mosaic dashboard

The code

<section class="tsl-14 w-full h-svh min-h-svh grid grid-cols-[68px_1fr] font-tsl14-sans bg-tsl14-bg text-tsl14-ink">
  <aside aria-label="Primary" class="relative flex flex-col items-center gap-1.5 py-3.5 bg-tsl14-panel border-r border-tsl14-line">
    <span aria-hidden="true" class="w-[30px] h-[30px] mb-3 rounded-[9px] bg-[conic-gradient(from_45deg,var(--color-tsl14-accent),oklch(0.62_0.17_285))]"></span>
    <span aria-hidden="true" class="tsl-14-pill absolute left-2 w-[3.5px] h-[26px] rounded"></span>
    <nav class="flex flex-col gap-1.5">
      <label data-tooltip="Home" class="relative z-[1] grid place-items-center w-11 h-11 rounded-xl text-tsl14-mut cursor-pointer transition-colors duration-200 hover:text-tsl14-ink has-checked:text-tsl14-accent has-checked:bg-tsl14-tint has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tsl14-accent has-[:focus-visible]:outline-offset-2 after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0 has-[:focus-visible]:after:opacity-100 has-[:focus-visible]:after:translate-x-0"><input type="radio" name="tsl14" checked class="absolute inset-0 m-0 opacity-0 cursor-pointer"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-5 h-5 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 10.5 12 3l9 7.5M5.5 9.5V21h13V9.5"/></svg><span class="sr-only">Home</span></label>
      <label data-tooltip="Inbox" class="relative z-[1] grid place-items-center w-11 h-11 rounded-xl text-tsl14-mut cursor-pointer transition-colors duration-200 hover:text-tsl14-ink has-checked:text-tsl14-accent has-checked:bg-tsl14-tint has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tsl14-accent has-[:focus-visible]:outline-offset-2 after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0 has-[:focus-visible]:after:opacity-100 has-[:focus-visible]:after:translate-x-0"><input type="radio" name="tsl14" class="absolute inset-0 m-0 opacity-0 cursor-pointer"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-5 h-5 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 13h5l2 3h4l2-3h5M5 5h14l2 8v6H3v-6z"/></svg><span class="sr-only">Inbox</span><i aria-hidden="true" class="absolute top-[9px] right-[9px] w-2 h-2 rounded-full bg-tsl14-alert shadow-[0_0_0_2.5px_var(--color-tsl14-panel)]"></i></label>
      <label data-tooltip="Projects" class="relative z-[1] grid place-items-center w-11 h-11 rounded-xl text-tsl14-mut cursor-pointer transition-colors duration-200 hover:text-tsl14-ink has-checked:text-tsl14-accent has-checked:bg-tsl14-tint has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tsl14-accent has-[:focus-visible]:outline-offset-2 after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0 has-[:focus-visible]:after:opacity-100 has-[:focus-visible]:after:translate-x-0"><input type="radio" name="tsl14" class="absolute inset-0 m-0 opacity-0 cursor-pointer"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-5 h-5 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="sr-only">Projects</span></label>
      <label data-tooltip="Analytics" class="relative z-[1] grid place-items-center w-11 h-11 rounded-xl text-tsl14-mut cursor-pointer transition-colors duration-200 hover:text-tsl14-ink has-checked:text-tsl14-accent has-checked:bg-tsl14-tint has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tsl14-accent has-[:focus-visible]:outline-offset-2 after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0 has-[:focus-visible]:after:opacity-100 has-[:focus-visible]:after:translate-x-0"><input type="radio" name="tsl14" class="absolute inset-0 m-0 opacity-0 cursor-pointer"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-5 h-5 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M4 20V10m6 10V4m6 16v-7"/></svg><span class="sr-only">Analytics</span></label>
    </nav>
    <span data-tooltip="Ada Kim" class="relative grid place-items-center w-11 h-11 mt-auto rounded-full text-[11.5px] font-bold text-white cursor-pointer bg-[linear-gradient(135deg,var(--color-tsl14-accent),oklch(0.6_0.17_285))] after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0">AK</span>
  </aside>
  <main class="p-8 overflow-y-auto [scrollbar-gutter:stable]">
    <h1 class="text-[22px] font-semibold leading-tight mb-2.5">Home</h1>
    <p class="max-w-[46ch] text-sm leading-relaxed text-tsl14-mut text-pretty">Click the rail icons &mdash; the active pill slides between them with CSS anchor positioning. Hover for tooltips.</p>
  </main>
</section>
@import "tailwindcss";

@theme {
  --color-tsl14-bg: #121519;
  --color-tsl14-bg: oklch(0.13 0.015 255);
  --color-tsl14-panel: #181b21;
  --color-tsl14-panel: oklch(0.16 0.02 255);
  --color-tsl14-tip: #262a32;
  --color-tsl14-tip: oklch(0.24 0.02 255);
  --color-tsl14-ink: #e9ebef;
  --color-tsl14-ink: oklch(0.93 0.005 255);
  --color-tsl14-mut: #82868f;
  --color-tsl14-mut: oklch(0.58 0.014 255);
  --color-tsl14-line: #373b44;
  --color-tsl14-line: oklch(0.27 0.02 255);
  --color-tsl14-accent: #4fc9e0;
  --color-tsl14-accent: oklch(0.74 0.14 195);
  --color-tsl14-alert: #e7603f;
  --color-tsl14-alert: oklch(0.68 0.21 25);
  --color-tsl14-tint: rgb(79 201 224 / 0.14);
  --color-tsl14-tint: color-mix(in oklab, oklch(0.74 0.14 195) 14%, transparent);
  --color-tsl14-glow: rgb(79 201 224 / 0.6);
  --color-tsl14-glow: color-mix(in oklab, oklch(0.74 0.14 195) 60%, transparent);
  --font-tsl14-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tsl-14 {
  width: 100%;
  height: 100svh;
  min-height: 100svh;
  color-scheme: dark;
}
/* Anchor names are identifiers, not design values — no utility mints them,
   and the sliding pill is pure enhancement over the has-checked: tint. */

.tsl-14 nav label:nth-child(1) {
  anchor-name: --tsl14-a1;
}

.tsl-14 nav label:nth-child(2) {
  anchor-name: --tsl14-a2;
}

.tsl-14 nav label:nth-child(3) {
  anchor-name: --tsl14-a3;
}

.tsl-14 nav label:nth-child(4) {
  anchor-name: --tsl14-a4;
}

.tsl-14-pill {
  background: var(--color-tsl14-accent);
  box-shadow: 0 0 10px var(--color-tsl14-glow);
  position-anchor: --tsl14-a1;
  top: calc(anchor(top) + 9px);
  transition: top .3s cubic-bezier(.5,0,.2,1.2);
}

.tsl-14:has(nav label:nth-child(2) input:checked) .tsl-14-pill {
  position-anchor: --tsl14-a2;
}

.tsl-14:has(nav label:nth-child(3) input:checked) .tsl-14-pill {
  position-anchor: --tsl14-a3;
}

.tsl-14:has(nav label:nth-child(4) input:checked) .tsl-14-pill {
  position-anchor: --tsl14-a4;
}

@supports not (anchor-name: --x) {
  .tsl-14-pill {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tsl-14-pill {
    transition: none;
  }
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<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.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tsl14-bg: #121519;
  --color-tsl14-bg: oklch(0.13 0.015 255);
  --color-tsl14-panel: #181b21;
  --color-tsl14-panel: oklch(0.16 0.02 255);
  --color-tsl14-tip: #262a32;
  --color-tsl14-tip: oklch(0.24 0.02 255);
  --color-tsl14-ink: #e9ebef;
  --color-tsl14-ink: oklch(0.93 0.005 255);
  --color-tsl14-mut: #82868f;
  --color-tsl14-mut: oklch(0.58 0.014 255);
  --color-tsl14-line: #373b44;
  --color-tsl14-line: oklch(0.27 0.02 255);
  --color-tsl14-accent: #4fc9e0;
  --color-tsl14-accent: oklch(0.74 0.14 195);
  --color-tsl14-alert: #e7603f;
  --color-tsl14-alert: oklch(0.68 0.21 25);
  --color-tsl14-tint: rgb(79 201 224 / 0.14);
  --color-tsl14-tint: color-mix(in oklab, oklch(0.74 0.14 195) 14%, transparent);
  --color-tsl14-glow: rgb(79 201 224 / 0.6);
  --color-tsl14-glow: color-mix(in oklab, oklch(0.74 0.14 195) 60%, transparent);
  --font-tsl14-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tsl-14 {
  width: 100%;
  height: 100svh;
  min-height: 100svh;
  color-scheme: dark;
}
/* Anchor names are identifiers, not design values — no utility mints them,
   and the sliding pill is pure enhancement over the has-checked: tint. */

.tsl-14 nav label:nth-child(1) {
  anchor-name: --tsl14-a1;
}

.tsl-14 nav label:nth-child(2) {
  anchor-name: --tsl14-a2;
}

.tsl-14 nav label:nth-child(3) {
  anchor-name: --tsl14-a3;
}

.tsl-14 nav label:nth-child(4) {
  anchor-name: --tsl14-a4;
}

.tsl-14-pill {
  background: var(--color-tsl14-accent);
  box-shadow: 0 0 10px var(--color-tsl14-glow);
  position-anchor: --tsl14-a1;
  top: calc(anchor(top) + 9px);
  transition: top .3s cubic-bezier(.5,0,.2,1.2);
}

.tsl-14:has(nav label:nth-child(2) input:checked) .tsl-14-pill {
  position-anchor: --tsl14-a2;
}

.tsl-14:has(nav label:nth-child(3) input:checked) .tsl-14-pill {
  position-anchor: --tsl14-a3;
}

.tsl-14:has(nav label:nth-child(4) input:checked) .tsl-14-pill {
  position-anchor: --tsl14-a4;
}

@supports not (anchor-name: --x) {
  .tsl-14-pill {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tsl-14-pill {
    transition: none;
  }
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • This is the collection's clearest split. Everything about the rail — sizing, tint, tooltip, focus ring, dot — is utilities; everything about the pill is CSS, because anchor positioning trades in identifiers and @supports guards, neither of which belongs in a class list.
  • has-[:focus-visible]: is what makes a label wrapping an invisible radio focusable-looking. It is worth knowing as the generic form: has-checked: is just sugar for this shape.
  • UI change from the CSS original: the avatar grew from 36px to 44px (it is a real control with a tooltip, so it owes the same target size as the nav items), and the tooltip now also fires on keyboard focus for every item rather than hover alone.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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 Mini Icon Rail Sidebar with Anchor-Positioned Active Pill
Source: https://codefronts.com/layouts/tailwind-css-sidebar-layouts/tailwind-mini-icon-rail-anchor-positioned-active-pill/

The 68px icon rail perfected: 44px targets, hover tooltips, notification dots, a bottom-pinned avatar &mdash; and an active pill that glides between icons using CSS anchor positioning. The effect teams reach for a motion library to get, with no script and a clean fallback.
## HTML
```html
<section class="tsl-14 w-full h-svh min-h-svh grid grid-cols-[68px_1fr] font-tsl14-sans bg-tsl14-bg text-tsl14-ink">
  <aside aria-label="Primary" class="relative flex flex-col items-center gap-1.5 py-3.5 bg-tsl14-panel border-r border-tsl14-line">
    <span aria-hidden="true" class="w-[30px] h-[30px] mb-3 rounded-[9px] bg-[conic-gradient(from_45deg,var(--color-tsl14-accent),oklch(0.62_0.17_285))]"></span>
    <span aria-hidden="true" class="tsl-14-pill absolute left-2 w-[3.5px] h-[26px] rounded"></span>
    <nav class="flex flex-col gap-1.5">
      <label data-tooltip="Home" class="relative z-[1] grid place-items-center w-11 h-11 rounded-xl text-tsl14-mut cursor-pointer transition-colors duration-200 hover:text-tsl14-ink has-checked:text-tsl14-accent has-checked:bg-tsl14-tint has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tsl14-accent has-[:focus-visible]:outline-offset-2 after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0 has-[:focus-visible]:after:opacity-100 has-[:focus-visible]:after:translate-x-0"><input type="radio" name="tsl14" checked class="absolute inset-0 m-0 opacity-0 cursor-pointer"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-5 h-5 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 10.5 12 3l9 7.5M5.5 9.5V21h13V9.5"/></svg><span class="sr-only">Home</span></label>
      <label data-tooltip="Inbox" class="relative z-[1] grid place-items-center w-11 h-11 rounded-xl text-tsl14-mut cursor-pointer transition-colors duration-200 hover:text-tsl14-ink has-checked:text-tsl14-accent has-checked:bg-tsl14-tint has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tsl14-accent has-[:focus-visible]:outline-offset-2 after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0 has-[:focus-visible]:after:opacity-100 has-[:focus-visible]:after:translate-x-0"><input type="radio" name="tsl14" class="absolute inset-0 m-0 opacity-0 cursor-pointer"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-5 h-5 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 13h5l2 3h4l2-3h5M5 5h14l2 8v6H3v-6z"/></svg><span class="sr-only">Inbox</span><i aria-hidden="true" class="absolute top-[9px] right-[9px] w-2 h-2 rounded-full bg-tsl14-alert shadow-[0_0_0_2.5px_var(--color-tsl14-panel)]"></i></label>
      <label data-tooltip="Projects" class="relative z-[1] grid place-items-center w-11 h-11 rounded-xl text-tsl14-mut cursor-pointer transition-colors duration-200 hover:text-tsl14-ink has-checked:text-tsl14-accent has-checked:bg-tsl14-tint has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tsl14-accent has-[:focus-visible]:outline-offset-2 after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0 has-[:focus-visible]:after:opacity-100 has-[:focus-visible]:after:translate-x-0"><input type="radio" name="tsl14" class="absolute inset-0 m-0 opacity-0 cursor-pointer"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-5 h-5 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="sr-only">Projects</span></label>
      <label data-tooltip="Analytics" class="relative z-[1] grid place-items-center w-11 h-11 rounded-xl text-tsl14-mut cursor-pointer transition-colors duration-200 hover:text-tsl14-ink has-checked:text-tsl14-accent has-checked:bg-tsl14-tint has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tsl14-accent has-[:focus-visible]:outline-offset-2 after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0 has-[:focus-visible]:after:opacity-100 has-[:focus-visible]:after:translate-x-0"><input type="radio" name="tsl14" class="absolute inset-0 m-0 opacity-0 cursor-pointer"><svg viewBox="0 0 24 24" aria-hidden="true" class="w-5 h-5 fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]"><path d="M4 20V10m6 10V4m6 16v-7"/></svg><span class="sr-only">Analytics</span></label>
    </nav>
    <span data-tooltip="Ada Kim" class="relative grid place-items-center w-11 h-11 mt-auto rounded-full text-[11.5px] font-bold text-white cursor-pointer bg-[linear-gradient(135deg,var(--color-tsl14-accent),oklch(0.6_0.17_285))] after:content-[attr(data-tooltip)] after:absolute after:left-[calc(100%+12px)] after:top-1/2 after:-translate-y-1/2 after:-translate-x-1 after:px-2.5 after:py-[7px] after:rounded-lg after:bg-tsl14-tip after:border after:border-tsl14-line after:text-tsl14-ink after:text-xs after:font-medium after:whitespace-nowrap after:opacity-0 after:pointer-events-none after:transition-[opacity,translate] after:duration-200 after:delay-[250ms] after:z-10 hover:after:opacity-100 hover:after:translate-x-0">AK</span>
  </aside>
  <main class="p-8 overflow-y-auto [scrollbar-gutter:stable]">
    <h1 class="text-[22px] font-semibold leading-tight mb-2.5">Home</h1>
    <p class="max-w-[46ch] text-sm leading-relaxed text-tsl14-mut text-pretty">Click the rail icons &mdash; the active pill slides between them with CSS anchor positioning. Hover for tooltips.</p>
  </main>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tsl14-bg: #121519;
  --color-tsl14-bg: oklch(0.13 0.015 255);
  --color-tsl14-panel: #181b21;
  --color-tsl14-panel: oklch(0.16 0.02 255);
  --color-tsl14-tip: #262a32;
  --color-tsl14-tip: oklch(0.24 0.02 255);
  --color-tsl14-ink: #e9ebef;
  --color-tsl14-ink: oklch(0.93 0.005 255);
  --color-tsl14-mut: #82868f;
  --color-tsl14-mut: oklch(0.58 0.014 255);
  --color-tsl14-line: #373b44;
  --color-tsl14-line: oklch(0.27 0.02 255);
  --color-tsl14-accent: #4fc9e0;
  --color-tsl14-accent: oklch(0.74 0.14 195);
  --color-tsl14-alert: #e7603f;
  --color-tsl14-alert: oklch(0.68 0.21 25);
  --color-tsl14-tint: rgb(79 201 224 / 0.14);
  --color-tsl14-tint: color-mix(in oklab, oklch(0.74 0.14 195) 14%, transparent);
  --color-tsl14-glow: rgb(79 201 224 / 0.6);
  --color-tsl14-glow: color-mix(in oklab, oklch(0.74 0.14 195) 60%, transparent);
  --font-tsl14-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tsl-14 {
  width: 100%;
  height: 100svh;
  min-height: 100svh;
  color-scheme: dark;
}
/* Anchor names are identifiers, not design values — no utility mints them,
   and the sliding pill is pure enhancement over the has-checked: tint. */

.tsl-14 nav label:nth-child(1) {
  anchor-name: --tsl14-a1;
}

.tsl-14 nav label:nth-child(2) {
  anchor-name: --tsl14-a2;
}

.tsl-14 nav label:nth-child(3) {
  anchor-name: --tsl14-a3;
}

.tsl-14 nav label:nth-child(4) {
  anchor-name: --tsl14-a4;
}

.tsl-14-pill {
  background: var(--color-tsl14-accent);
  box-shadow: 0 0 10px var(--color-tsl14-glow);
  position-anchor: --tsl14-a1;
  top: calc(anchor(top) + 9px);
  transition: top .3s cubic-bezier(.5,0,.2,1.2);
}

.tsl-14:has(nav label:nth-child(2) input:checked) .tsl-14-pill {
  position-anchor: --tsl14-a2;
}

.tsl-14:has(nav label:nth-child(3) input:checked) .tsl-14-pill {
  position-anchor: --tsl14-a3;
}

.tsl-14:has(nav label:nth-child(4) input:checked) .tsl-14-pill {
  position-anchor: --tsl14-a4;
}

@supports not (anchor-name: --x) {
  .tsl-14-pill {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tsl-14-pill {
    transition: none;
  }
}
```

How this works

The pill is a shared layout element, natively. Each item declares an anchor name; the pill re-points position-anchor when a different radio is checked, and because its top is a transitioned anchor() expression, re-targeting makes it slide. The whole mechanism is four short CSS rules and no markup beyond one absolutely-positioned span.

The fallback is the design, not a degradation. Every item also carries has-checked: colour and tint utilities, so in Firefox the rail reads as a perfectly normal, perfectly clear icon nav; @supports not (anchor-name:--x) simply hides the pill.

Make it yours

  • Rail widths: 56px for dense pro tools, 68px default, 80px touch-first.
  • Tooltip delay: 250ms stops flicker as the cursor travels; 500ms for rails beside a frequent mouse path.
  • Dots to counts: swap the 8px dot for a min-w-4 pill at 9.5px and you have numeric badges.

Gotchas — read before shipping

  • Ship the non-anchor active state first and treat the pill as bonus — Firefox has no anchor positioning yet.
  • Icon-only nav MUST keep an accessible name: each label here wraps its radio and carries a sr-only span as well as the tooltip attribute.
  • The notification dot needs a 2.5px ring in the rail colour, or it merges visually with the icon strokes underneath.

Browser support

ChromeSafariFirefoxEdge
125+26+—125+

The sliding pill needs CSS anchor positioning (Firefox in development); the rail with static active states works back to 2017.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…