Tailwind Sidebar: 16 Examples for Dashboards, Docs and Apps

Tailwind CSS has no sidebar component. A Tailwind sidebar is an aside with fixed or sticky positioning and a breakpoint prefix that hides it on small screens, and on phones a toggle slides it back in as a drawer.

Below are 16 Tailwind sidebar examples you can copy, followed by a guide to layout, drawers, collapse state, active links, dark mode and accessibility.

By Bogdan Sandu ยท Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

App shells and layout

Full layouts where the sidebar and the content share the screen: a column from the md breakpoint that becomes a dialog drawer on phones, a rail that collapses to icons, a panel you can resize and a sidebar themed with its own @theme tokens.

App shell with a dialog drawer on phones

  • hidden md:flex
  • <dialog>
  • starting:open:
  • backdrop:
  • min-w-0

On a phone, tap the menu button to open the drawer.

From 768px the aside is a flex column with hidden md:flex and a fixed w-64, and the content column takes the rest with flex-1 min-w-0. On phones the same links live in a native dialog opened with showModal(), so the browser keeps focus inside it, makes the page inert, closes on Escape and returns focus to the menu button. The slide comes from -translate-x-full, open:translate-x-0 and starting:open: inside motion-safe:, and the script only opens, closes on a backdrop or link click, and keeps aria-expanded in sync.

View code
<div class="flex min-h-[560px] w-full bg-stone-50 text-stone-900">
  <aside class="hidden w-64 shrink-0 flex-col border-r border-stone-200 bg-white md:flex" aria-label="Sidebar">
    <div class="flex h-16 items-center px-5">
      <a href="#" class="flex items-center gap-2.5 rounded-lg text-base font-semibold text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-700">
        <span class="grid size-8 place-items-center rounded-lg bg-emerald-700 text-white"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 19c0-8 5-14 15-14 0 10-6 15-14 15M5 19l7-7"/></svg></span>
        Fieldbook
      </a>
    </div>
    <nav aria-label="Main" class="flex-1 overflow-y-auto px-3 py-2">
      <ul class="space-y-1">
        <li><a href="#" aria-current="page" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 11l9-7 9 7M5 9.5V20h5v-6h4v6h5V9.5"/></svg>Overview</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 20V11M12 20V5M19 20v-6"/></svg>Harvests</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/></svg>Fields</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 19c0-2.8-2.2-5-5-5s-5 2.2-5 5M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM16 11a3 3 0 0 0 0-6M20 19c0-2.4-1.4-4.3-3.4-4.8"/></svg>Crew</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16v14H4zM4 10h16M8 3v4M16 3v4"/></svg>Schedule</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 3h7l5 5v13H7zM14 3v5h5"/></svg>Reports</a></li>
      </ul>
    </nav>
    <div class="m-3 rounded-xl border border-emerald-100 bg-emerald-50 p-4">
      <p class="text-sm font-semibold text-emerald-900">Frost risk on Thursday</p>
      <p class="mt-1 text-sm text-emerald-800">Cover the north beds before 6 PM.</p>
    </div>
  </aside>
  <dialog id="tsb-shell-drawer" aria-label="Main menu" class="m-0 h-full max-h-none w-72 max-w-[calc(100%-3rem)] bg-white p-0 text-stone-900 shadow-xl -translate-x-full open:translate-x-0 motion-safe:transition-[translate,display,overlay] motion-safe:transition-discrete motion-safe:duration-300 motion-safe:ease-out motion-safe:starting:open:-translate-x-full backdrop:bg-stone-950/0 open:backdrop:bg-stone-950/50 motion-safe:backdrop:transition-[background-color,display,overlay] motion-safe:backdrop:transition-discrete motion-safe:backdrop:duration-300 motion-safe:starting:open:backdrop:bg-stone-950/0">
    <div class="flex h-full flex-col">
      <div class="flex h-16 items-center justify-between px-5">
        <a href="#" class="flex items-center gap-2.5 rounded-lg text-base font-semibold text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-700">
        <span class="grid size-8 place-items-center rounded-lg bg-emerald-700 text-white"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 19c0-8 5-14 15-14 0 10-6 15-14 15M5 19l7-7"/></svg></span>
        Fieldbook
      </a>
        <button type="button" class="tsb-shell-close -mr-2 grid size-11 place-items-center rounded-lg text-stone-500 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-emerald-700">
          <span class="sr-only">Close menu</span>
          <svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>
        </button>
      </div>
      <nav aria-label="Main" class="flex-1 overflow-y-auto px-3 py-2">
        <ul class="space-y-1">
        <li><a href="#" aria-current="page" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 11l9-7 9 7M5 9.5V20h5v-6h4v6h5V9.5"/></svg>Overview</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 20V11M12 20V5M19 20v-6"/></svg>Harvests</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/></svg>Fields</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 19c0-2.8-2.2-5-5-5s-5 2.2-5 5M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM16 11a3 3 0 0 0 0-6M20 19c0-2.4-1.4-4.3-3.4-4.8"/></svg>Crew</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16v14H4zM4 10h16M8 3v4M16 3v4"/></svg>Schedule</a></li>
        <li><a href="#" class="group flex min-h-11 items-center gap-3 rounded-lg px-3 text-sm font-medium text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 aria-[current=page]:bg-emerald-50 aria-[current=page]:text-emerald-800"><svg class="size-5 shrink-0 text-stone-400 group-hover:text-stone-600 group-aria-[current=page]:text-emerald-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 3h7l5 5v13H7zM14 3v5h5"/></svg>Reports</a></li>
        </ul>
      </nav>
    </div>
  </dialog>
  <div class="flex min-w-0 flex-1 flex-col">
    <header class="flex h-16 items-center gap-3 border-b border-stone-200 bg-white px-4 md:px-8">
      <button type="button" id="tsb-shell-open" aria-haspopup="dialog" aria-controls="tsb-shell-drawer" aria-expanded="false" class="-ml-2 grid size-11 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-emerald-700 md:hidden">
        <span class="sr-only">Open menu</span>
        <svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
      </button>
      <h2 class="text-lg font-semibold">Overview</h2>
      <span class="ml-auto rounded-full bg-stone-100 px-3 py-1 text-xs font-medium text-stone-600">Week 43</span>
    </header>
    <div class="space-y-6 p-4 md:p-8">
      <div class="grid grid-cols-2 gap-3 md:grid-cols-3 md:gap-4">
        <div class="rounded-xl border border-stone-200 bg-white p-4"><p class="text-sm text-stone-500">Acres harvested</p><p class="mt-1 text-2xl font-semibold">412</p></div>
        <div class="rounded-xl border border-stone-200 bg-white p-4"><p class="text-sm text-stone-500">Crew on shift</p><p class="mt-1 text-2xl font-semibold">18</p></div>
        <div class="col-span-2 rounded-xl border border-stone-200 bg-white p-4 md:col-span-1"><p class="text-sm text-stone-500">Bins in cold storage</p><p class="mt-1 text-2xl font-semibold">96</p></div>
      </div>
      <div class="rounded-xl border border-stone-200 bg-white">
        <h3 class="border-b border-stone-200 px-4 py-3 text-sm font-semibold">Today</h3>
        <ul class="divide-y divide-stone-100 text-sm">
          <li class="flex items-center justify-between gap-3 px-4 py-3"><span>Pick block 7, Honeycrisp</span><span class="shrink-0 text-stone-500">7:30 AM</span></li>
          <li class="flex items-center justify-between gap-3 px-4 py-3"><span>Irrigation check, south field</span><span class="shrink-0 text-stone-500">11:00 AM</span></li>
          <li class="flex items-center justify-between gap-3 px-4 py-3"><span>Load truck for Hillside Market</span><span class="shrink-0 text-stone-500">3:15 PM</span></li>
        </ul>
      </div>
    </div>
  </div>
</div>

Collapsible icon rail with tooltips

  • group-data-[state=collapsed]/rail:
  • sr-only
  • group-hover/item:
  • aria-expanded
  • transition-[width]

Click Collapse sidebar, then hover or tab through the icons.

The aside is a named group (group/rail) with a data-state attribute, and every child reacts to it: data-[state=collapsed]:w-[4.5rem] narrows the panel, the labels switch to sr-only so each link keeps its accessible name, and tooltips appear with stacked variants such as group-data-[state=collapsed]/rail:group-hover/item:block. The script flips data-state and aria-expanded on the toggle and starts collapsed on narrow screens. The nav has no overflow-y-auto on purpose, because a scrolling panel would clip the tooltips.

View code
<div class="flex min-h-[520px] w-full bg-slate-100 text-slate-900">
  <aside id="tsb-rail" data-state="expanded" aria-label="Sidebar" class="group/rail flex w-60 shrink-0 flex-col border-r border-slate-200 bg-white data-[state=collapsed]:w-[4.5rem] motion-safe:transition-[width] motion-safe:duration-200">
    <div class="flex h-16 items-center gap-2.5 overflow-hidden px-4 group-data-[state=collapsed]/rail:justify-center group-data-[state=collapsed]/rail:px-0">
      <span class="grid size-9 shrink-0 place-items-center rounded-lg bg-sky-600 text-white"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 3L5 14h6l-1 7 8-11h-6z"/></svg></span>
      <span class="truncate text-base font-semibold group-data-[state=collapsed]/rail:sr-only">Lumen</span>
    </div>
    <nav aria-label="Main" class="flex-1 px-3 py-2">
      <ul class="space-y-1">
        <li class="tsb-rail-item group/item relative">
          <a href="#" aria-current="page" class="flex h-11 items-center gap-3 overflow-hidden rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600 aria-[current=page]:bg-sky-50 aria-[current=page]:text-sky-800 group-data-[state=collapsed]/rail:justify-center group-data-[state=collapsed]/rail:px-0"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 11l9-7 9 7M5 9.5V20h5v-6h4v6h5V9.5"/></svg><span class="truncate group-data-[state=collapsed]/rail:sr-only">Dashboard</span></a>
          <span aria-hidden="true" class="pointer-events-none absolute top-1/2 left-full z-20 ml-2 hidden -translate-y-1/2 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium whitespace-nowrap text-white shadow-lg group-data-[state=collapsed]/rail:group-hover/item:block group-data-[state=collapsed]/rail:group-focus-within/item:block">Dashboard</span>
        </li>
        <li class="tsb-rail-item group/item relative">
          <a href="#" class="flex h-11 items-center gap-3 overflow-hidden rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600 aria-[current=page]:bg-sky-50 aria-[current=page]:text-sky-800 group-data-[state=collapsed]/rail:justify-center group-data-[state=collapsed]/rail:px-0"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="truncate group-data-[state=collapsed]/rail:sr-only">Projects</span></a>
          <span aria-hidden="true" class="pointer-events-none absolute top-1/2 left-full z-20 ml-2 hidden -translate-y-1/2 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium whitespace-nowrap text-white shadow-lg group-data-[state=collapsed]/rail:group-hover/item:block group-data-[state=collapsed]/rail:group-focus-within/item:block">Projects</span>
        </li>
        <li class="tsb-rail-item group/item relative">
          <a href="#" class="flex h-11 items-center gap-3 overflow-hidden rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600 aria-[current=page]:bg-sky-50 aria-[current=page]:text-sky-800 group-data-[state=collapsed]/rail:justify-center group-data-[state=collapsed]/rail:px-0"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16v14H4zM4 10h16M8 3v4M16 3v4"/></svg><span class="truncate group-data-[state=collapsed]/rail:sr-only">Calendar</span></a>
          <span aria-hidden="true" class="pointer-events-none absolute top-1/2 left-full z-20 ml-2 hidden -translate-y-1/2 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium whitespace-nowrap text-white shadow-lg group-data-[state=collapsed]/rail:group-hover/item:block group-data-[state=collapsed]/rail:group-focus-within/item:block">Calendar</span>
        </li>
        <li class="tsb-rail-item group/item relative">
          <a href="#" class="flex h-11 items-center gap-3 overflow-hidden rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600 aria-[current=page]:bg-sky-50 aria-[current=page]:text-sky-800 group-data-[state=collapsed]/rail:justify-center group-data-[state=collapsed]/rail:px-0"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 13h5l1.5 3h5l1.5-3h5M5.5 5h13L21 13v6H3v-6z"/></svg><span class="truncate group-data-[state=collapsed]/rail:sr-only">Messages</span></a>
          <span aria-hidden="true" class="pointer-events-none absolute top-1/2 left-full z-20 ml-2 hidden -translate-y-1/2 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium whitespace-nowrap text-white shadow-lg group-data-[state=collapsed]/rail:group-hover/item:block group-data-[state=collapsed]/rail:group-focus-within/item:block">Messages</span>
        </li>
        <li class="tsb-rail-item group/item relative">
          <a href="#" class="flex h-11 items-center gap-3 overflow-hidden rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600 aria-[current=page]:bg-sky-50 aria-[current=page]:text-sky-800 group-data-[state=collapsed]/rail:justify-center group-data-[state=collapsed]/rail:px-0"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 19c0-2.8-2.2-5-5-5s-5 2.2-5 5M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM16 11a3 3 0 0 0 0-6M20 19c0-2.4-1.4-4.3-3.4-4.8"/></svg><span class="truncate group-data-[state=collapsed]/rail:sr-only">Team</span></a>
          <span aria-hidden="true" class="pointer-events-none absolute top-1/2 left-full z-20 ml-2 hidden -translate-y-1/2 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium whitespace-nowrap text-white shadow-lg group-data-[state=collapsed]/rail:group-hover/item:block group-data-[state=collapsed]/rail:group-focus-within/item:block">Team</span>
        </li>
        <li class="tsb-rail-item group/item relative">
          <a href="#" class="flex h-11 items-center gap-3 overflow-hidden rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600 aria-[current=page]:bg-sky-50 aria-[current=page]:text-sky-800 group-data-[state=collapsed]/rail:justify-center group-data-[state=collapsed]/rail:px-0"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 20V11M12 20V5M19 20v-6"/></svg><span class="truncate group-data-[state=collapsed]/rail:sr-only">Reports</span></a>
          <span aria-hidden="true" class="pointer-events-none absolute top-1/2 left-full z-20 ml-2 hidden -translate-y-1/2 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium whitespace-nowrap text-white shadow-lg group-data-[state=collapsed]/rail:group-hover/item:block group-data-[state=collapsed]/rail:group-focus-within/item:block">Reports</span>
        </li>
      </ul>
    </nav>
    <div class="border-t border-slate-200 p-3">
      <button type="button" id="tsb-rail-toggle" aria-controls="tsb-rail" aria-expanded="true" class="flex h-11 w-full items-center gap-3 overflow-hidden rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600 group-data-[state=collapsed]/rail:justify-center group-data-[state=collapsed]/rail:px-0">
        <svg class="size-5 shrink-0 group-data-[state=collapsed]/rail:rotate-180 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 7l-5 5 5 5M18 7l-5 5 5 5"/></svg>
        <span id="tsb-rail-toggle-label" class="truncate group-data-[state=collapsed]/rail:sr-only">Collapse sidebar</span>
      </button>
    </div>
  </aside>
  <div class="min-w-0 flex-1 p-4 md:p-8">
    <h2 class="text-lg font-semibold">Dashboard</h2>
    <p class="mt-1 text-sm text-slate-600">Tuesday, 14 projects in flight.</p>
    <div class="mt-6 grid gap-3 md:grid-cols-3 md:gap-4">
      <div class="rounded-xl bg-white p-4 shadow-xs ring-1 ring-slate-200"><p class="text-sm text-slate-500">Open projects</p><p class="mt-1 text-2xl font-semibold">14</p></div>
      <div class="rounded-xl bg-white p-4 shadow-xs ring-1 ring-slate-200"><p class="text-sm text-slate-500">Due this week</p><p class="mt-1 text-2xl font-semibold">6</p></div>
      <div class="rounded-xl bg-white p-4 shadow-xs ring-1 ring-slate-200"><p class="text-sm text-slate-500">Hours logged</p><p class="mt-1 text-2xl font-semibold">128</p></div>
    </div>
  </div>
</div>

Resizable sidebar with a keyboard handle

  • w-(--tsb-w)
  • role="separator"
  • cursor-col-resize
  • touch-none
  • group-focus-visible:

Drag the line between the panels, or focus it and press the arrow keys.

The panel width lives in one CSS variable on the wrapper, and w-(--tsb-w) is the v4 shorthand for width: var(--tsb-w), so the script never touches a class. The handle is a focusable role="separator" with aria-valuenow, min and max, cursor-col-resize and touch-none for pointer dragging, and group-hover: plus group-focus-visible: thicken the line. The script handles pointer capture, the arrow keys (Shift for bigger steps), Home and End, and clamps the width to 60 percent of the frame.

View code
<div id="tsb-rs" class="flex min-h-[440px] w-full bg-zinc-50 text-zinc-900" style="--tsb-w: 16rem">
  <aside id="tsb-rs-panel" aria-label="Files" class="w-(--tsb-w) shrink-0 overflow-y-auto border-r border-zinc-200 bg-white">
    <div class="flex h-14 items-center justify-between gap-2 border-b border-zinc-200 px-3">
      <h3 class="truncate text-xs font-semibold tracking-wide text-zinc-600 uppercase">Explorer</h3>
      <button type="button" class="grid size-9 shrink-0 place-items-center rounded-md text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-amber-600"><span class="sr-only">New file</span><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button>
    </div>
    <nav aria-label="Project files" class="p-2">
      <ul class="space-y-px">
          <li><a href="#" class="flex min-h-9 items-center gap-2 rounded-md px-2 text-sm text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-amber-50 aria-[current=page]:font-medium aria-[current=page]:text-amber-900 "><svg class="size-4 shrink-0 text-amber-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="truncate">brand-kit</span></a></li>
          <li><a href="#" class="flex min-h-9 items-center gap-2 rounded-md px-2 text-sm text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-amber-50 aria-[current=page]:font-medium aria-[current=page]:text-amber-900 pl-6"><svg class="size-4 shrink-0 text-amber-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="truncate">assets</span></a></li>
          <li><a href="#" class="flex min-h-9 items-center gap-2 rounded-md px-2 text-sm text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-amber-50 aria-[current=page]:font-medium aria-[current=page]:text-amber-900 pl-10"><svg class="size-4 shrink-0 text-zinc-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4"/></svg><span class="truncate">logo-mark.svg</span></a></li>
          <li><a href="#" class="flex min-h-9 items-center gap-2 rounded-md px-2 text-sm text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-amber-50 aria-[current=page]:font-medium aria-[current=page]:text-amber-900 pl-10"><svg class="size-4 shrink-0 text-zinc-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4"/></svg><span class="truncate">palette.json</span></a></li>
          <li><a href="#" class="flex min-h-9 items-center gap-2 rounded-md px-2 text-sm text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-amber-50 aria-[current=page]:font-medium aria-[current=page]:text-amber-900 pl-6"><svg class="size-4 shrink-0 text-amber-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="truncate">docs</span></a></li>
          <li><a href="#" aria-current="page" class="flex min-h-9 items-center gap-2 rounded-md px-2 text-sm text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-amber-50 aria-[current=page]:font-medium aria-[current=page]:text-amber-900 pl-10"><svg class="size-4 shrink-0 text-zinc-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4"/></svg><span class="truncate">brand-guidelines.md</span></a></li>
          <li><a href="#" class="flex min-h-9 items-center gap-2 rounded-md px-2 text-sm text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-amber-50 aria-[current=page]:font-medium aria-[current=page]:text-amber-900 pl-10"><svg class="size-4 shrink-0 text-zinc-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4"/></svg><span class="truncate">voice-and-tone.md</span></a></li>
          <li><a href="#" class="flex min-h-9 items-center gap-2 rounded-md px-2 text-sm text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-amber-50 aria-[current=page]:font-medium aria-[current=page]:text-amber-900 pl-6"><svg class="size-4 shrink-0 text-zinc-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4"/></svg><span class="truncate">README.md</span></a></li>
          <li><a href="#" class="flex min-h-9 items-center gap-2 rounded-md px-2 text-sm text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-amber-600 aria-[current=page]:bg-amber-50 aria-[current=page]:font-medium aria-[current=page]:text-amber-900 pl-6"><svg class="size-4 shrink-0 text-zinc-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4"/></svg><span class="truncate">package.json</span></a></li>
      </ul>
    </nav>
  </aside>
  <div id="tsb-rs-handle" role="separator" tabindex="0" aria-orientation="vertical" aria-controls="tsb-rs-panel" aria-label="Resize file panel" aria-valuemin="160" aria-valuemax="420" aria-valuenow="256" class="group relative -ml-1 w-2 shrink-0 cursor-col-resize touch-none focus-visible:outline-2 focus-visible:outline-amber-600">
    <span class="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-zinc-200 group-hover:w-1 group-hover:bg-amber-500 group-focus-visible:w-1 group-focus-visible:bg-amber-600 group-active:w-1 group-active:bg-amber-600"></span>
  </div>
  <div class="min-w-0 flex-1 p-4 md:p-8">
    <div class="flex flex-wrap items-center justify-between gap-2">
      <h2 class="truncate text-base font-semibold">brand-guidelines.md</h2>
      <p class="text-xs text-zinc-500">Panel width <output id="tsb-rs-out" class="font-mono font-semibold text-zinc-800">256px</output></p>
    </div>
    <div class="mt-4 space-y-3 rounded-xl border border-zinc-200 bg-white p-4 text-sm leading-relaxed text-zinc-700 md:p-6">
      <p class="font-semibold text-zinc-900">Clear space</p>
      <p>Keep a margin around the mark equal to the height of the letter K. Nothing else sits inside that area, including photo edges.</p>
      <p class="font-semibold text-zinc-900">Minimum size</p>
      <p>The full logo stays at 96px wide or larger on screens. Below that, switch to the mark on its own.</p>
    </div>
  </div>
</div>

Sidebar themed with @theme tokens

  • @theme
  • bg-tsb-panel
  • rounded-tsb
  • md:w-tsb-rail
  • grid-cols-2 md:flex-col

Six prefixed variables in @theme turn into real utilities: --color-tsb-panel gives bg-tsb-panel and text-tsb-panel, --radius-tsb gives rounded-tsb and --container-tsb-rail gives md:w-tsb-rail. Rebranding the sidebar means editing those lines, not hunting classes, and the tokens also exist as CSS variables. On phones the links sit in a two-column grid above the content and switch to a column from md:.

View code
<div class="flex min-h-[500px] w-full flex-col bg-[#f6f3ea] text-[#1d2a24] md:flex-row">
  <aside aria-label="Sidebar" class="flex w-full flex-col gap-6 bg-tsb-panel p-5 text-tsb-ink md:w-tsb-rail md:shrink-0">
    <a href="#" class="flex items-center gap-3 rounded-tsb focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tsb-accent">
      <span class="grid size-10 place-items-center rounded-tsb bg-tsb-accent text-tsb-panel"><svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 19c0-8 5-14 15-14 0 10-6 15-14 15M5 19l7-7"/></svg></span>
      <span><span class="block text-base font-semibold">Fernhill</span><span class="block text-xs text-tsb-muted">Nursery operations</span></span>
    </a>
    <nav aria-label="Main">
      <ul class="grid grid-cols-2 gap-1 md:flex md:flex-col">
        <li><a href="#" aria-current="page" class="flex min-h-11 items-center gap-3 rounded-tsb px-3 text-sm font-medium text-tsb-muted hover:bg-tsb-hover hover:text-tsb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tsb-accent aria-[current=page]:bg-tsb-accent aria-[current=page]:text-tsb-panel"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 19c0-8 5-14 15-14 0 10-6 15-14 15M5 19l7-7"/></svg>Greenhouses</a></li>
        <li><a href="#" class="flex min-h-11 items-center gap-3 rounded-tsb px-3 text-sm font-medium text-tsb-muted hover:bg-tsb-hover hover:text-tsb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tsb-accent aria-[current=page]:bg-tsb-accent aria-[current=page]:text-tsb-panel"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7l9-4 9 4v10l-9 4-9-4zM3 7l9 4 9-4M12 11v10"/></svg>Orders</a></li>
        <li><a href="#" class="flex min-h-11 items-center gap-3 rounded-tsb px-3 text-sm font-medium text-tsb-muted hover:bg-tsb-hover hover:text-tsb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tsb-accent aria-[current=page]:bg-tsb-accent aria-[current=page]:text-tsb-panel"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/></svg>Stock</a></li>
        <li><a href="#" class="flex min-h-11 items-center gap-3 rounded-tsb px-3 text-sm font-medium text-tsb-muted hover:bg-tsb-hover hover:text-tsb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tsb-accent aria-[current=page]:bg-tsb-accent aria-[current=page]:text-tsb-panel"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h11v10H3zM14 10h4l3 3v3h-7M7 19a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM17 19a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></svg>Deliveries</a></li>
        <li><a href="#" class="flex min-h-11 items-center gap-3 rounded-tsb px-3 text-sm font-medium text-tsb-muted hover:bg-tsb-hover hover:text-tsb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tsb-accent aria-[current=page]:bg-tsb-accent aria-[current=page]:text-tsb-panel"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 19c0-2.8-2.2-5-5-5s-5 2.2-5 5M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM16 11a3 3 0 0 0 0-6M20 19c0-2.4-1.4-4.3-3.4-4.8"/></svg>Customers</a></li>
        <li><a href="#" class="flex min-h-11 items-center gap-3 rounded-tsb px-3 text-sm font-medium text-tsb-muted hover:bg-tsb-hover hover:text-tsb-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tsb-accent aria-[current=page]:bg-tsb-accent aria-[current=page]:text-tsb-panel"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2z"/></svg>Settings</a></li>
      </ul>
    </nav>
    <div class="mt-auto rounded-tsb bg-tsb-hover p-4">
      <p class="flex items-center gap-2 text-sm font-semibold text-tsb-ink"><svg class="size-4 text-tsb-accent" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4l9 16H3zM12 10v4M12 17h.01"/></svg>Frost tonight</p>
      <p class="mt-1 text-sm text-tsb-muted">Low of -2&deg;C. Heaters in houses B and C switch on at 9 PM.</p>
    </div>
  </aside>
  <div class="min-w-0 flex-1 p-4 md:p-8">
    <h2 class="text-lg font-semibold">Greenhouses</h2>
    <ul class="mt-4 grid gap-3 md:grid-cols-3">
      <li class="rounded-tsb bg-white p-4 ring-1 ring-[#e3dccb]"><p class="text-sm font-semibold">House A</p><p class="text-sm text-[#5b6b62]">Seedlings, 21&deg;C</p><p class="mt-3 text-xs font-medium text-[#2f6b4f]">Humidity 64%</p></li>
      <li class="rounded-tsb bg-white p-4 ring-1 ring-[#e3dccb]"><p class="text-sm font-semibold">House B</p><p class="text-sm text-[#5b6b62]">Citrus, 17&deg;C</p><p class="mt-3 text-xs font-medium text-[#2f6b4f]">Humidity 58%</p></li>
      <li class="rounded-tsb bg-white p-4 ring-1 ring-[#e3dccb]"><p class="text-sm font-semibold">House C</p><p class="text-sm text-[#5b6b62]">Ferns, 19&deg;C</p><p class="mt-3 text-xs font-medium text-[#9a4b16]">Humidity 81%, vent open</p></li>
    </ul>
  </div>
</div>

Sidebar navigation patterns

What goes inside the panel: section headings, badges and an account menu, a filter box, has-[[aria-current=page]]: tricks that style whole sections from the current link, and flyout submenus that escape a scrolling rail.

Sections, badges and a user menu

  • aria-labelledby
  • group-aria-[current=page]:
  • popover
  • truncate
  • mt-auto

Click the account button at the bottom of the sidebar.

Each section is a heading plus a list tied to it with aria-labelledby, so screen readers announce Workspace or Teams when entering the list. Badges are spans inside the link with sr-only words after the number, and group-aria-[current=page]: turns the badge and icon violet on the current row. The account button at the bottom opens a native popover, which gives light dismiss and Escape for free; the script only places it above the button and mirrors its state in aria-expanded.

View code
<div class="flex min-h-[600px] w-full bg-slate-50 text-slate-900">
  <aside aria-label="Sidebar" class="flex w-full flex-col border-r border-slate-200 bg-white md:w-64 md:shrink-0">
    <div class="flex h-16 items-center justify-between gap-2 px-5">
      <a href="#" class="flex items-center gap-2.5 rounded-lg text-base font-semibold focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-violet-600">
        <span class="grid size-8 place-items-center rounded-lg bg-violet-600 text-white"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 20V11M12 20V5M19 20v-6"/></svg></span>
        Tidewater
      </a>
      <button type="button" class="relative -mr-2 grid size-11 place-items-center rounded-lg text-slate-500 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600">
        <span class="sr-only">Notifications, 3 unread</span>
        <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 16v-5a6 6 0 1 0-12 0v5l-2 2h16zM10 21h4"/></svg>
        <span class="absolute top-2.5 right-2.5 size-2 rounded-full bg-rose-500 ring-2 ring-white" aria-hidden="true"></span>
      </button>
    </div>
    <nav aria-label="Main" class="flex-1 space-y-6 px-3 py-3">
      <div>
        <h3 id="tsb-sb-h-work" class="px-3 pb-2 text-xs font-semibold tracking-wide text-slate-500 uppercase">Workspace</h3>
        <ul aria-labelledby="tsb-sb-h-work" class="space-y-0.5">
          <li><a href="#" aria-current="page" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><svg class="size-5 shrink-0 text-slate-400 group-aria-[current=page]:text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 13h5l1.5 3h5l1.5-3h5M5.5 5h13L21 13v6H3v-6z"/></svg><span class="truncate">Inbox</span><span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600 group-aria-[current=page]:bg-violet-600 group-aria-[current=page]:text-white">12<span class="sr-only"> unread</span></span></a></li>
          <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><svg class="size-5 shrink-0 text-slate-400 group-aria-[current=page]:text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"/></svg><span class="truncate">My issues</span><span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600 group-aria-[current=page]:bg-violet-600 group-aria-[current=page]:text-white">4<span class="sr-only"> assigned</span></span></a></li>
          <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><svg class="size-5 shrink-0 text-slate-400 group-aria-[current=page]:text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><span class="truncate">Projects</span></a></li>
          <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><svg class="size-5 shrink-0 text-slate-400 group-aria-[current=page]:text-violet-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16v14H4zM4 10h16M8 3v4M16 3v4"/></svg><span class="truncate">Cycles</span></a></li>
        </ul>
      </div>
      <div>
        <h3 id="tsb-sb-h-teams" class="px-3 pb-2 text-xs font-semibold tracking-wide text-slate-500 uppercase">Teams</h3>
        <ul aria-labelledby="tsb-sb-h-teams" class="space-y-0.5">
          <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><span class="size-2.5 shrink-0 rounded-sm bg-pink-500" aria-hidden="true"></span><span class="truncate">Design</span><span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600 group-aria-[current=page]:bg-violet-600 group-aria-[current=page]:text-white">7<span class="sr-only"> open issues</span></span></a></li>
          <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><span class="size-2.5 shrink-0 rounded-sm bg-cyan-500" aria-hidden="true"></span><span class="truncate">Platform</span><span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600 group-aria-[current=page]:bg-violet-600 group-aria-[current=page]:text-white">23<span class="sr-only"> open issues</span></span></a></li>
          <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600 aria-[current=page]:bg-violet-50 aria-[current=page]:text-violet-800"><span class="size-2.5 shrink-0 rounded-sm bg-lime-500" aria-hidden="true"></span><span class="truncate">Growth</span><span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600 group-aria-[current=page]:bg-violet-600 group-aria-[current=page]:text-white">9<span class="sr-only"> open issues</span></span></a></li>
        </ul>
      </div>
    </nav>
    <div class="border-t border-slate-200 p-3">
      <button type="button" id="tsb-sb-user" popovertarget="tsb-sb-menu" aria-expanded="false" class="flex w-full items-center gap-3 rounded-lg p-2 text-left hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-violet-600 aria-expanded:bg-slate-100">
        <span class="grid size-9 shrink-0 place-items-center rounded-full bg-violet-600 text-sm font-semibold text-white" aria-hidden="true">MR</span>
        <span class="min-w-0 flex-1"><span class="block truncate text-sm font-semibold">Mara Reyes</span><span class="block truncate text-xs text-slate-500">mara@tidewater.example</span></span>
        <svg class="size-4 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 9l4-4 4 4M8 15l4 4 4-4"/></svg>
      </button>
      <div id="tsb-sb-menu" popover class="inset-auto m-0 w-56 rounded-xl border border-slate-200 bg-white p-1.5 text-slate-900 shadow-lg">
        <ul class="space-y-0.5">
          <li><a href="#" class="flex min-h-10 w-full items-center gap-3 rounded-lg px-2.5 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg class="size-4 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21c0-4 3.6-7 8-7s8 3 8 7"/></svg>Profile</a></li>
          <li><a href="#" class="flex min-h-10 w-full items-center gap-3 rounded-lg px-2.5 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg class="size-4 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2z"/></svg>Preferences</a></li>
          <li><a href="#" class="flex min-h-10 w-full items-center gap-3 rounded-lg px-2.5 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg class="size-4 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7h18v10H3zM7 11h.01M11 11h.01M15 11h.01M8 14h8"/></svg>Keyboard shortcuts</a></li>
        </ul>
        <div class="my-1.5 h-px bg-slate-200"></div>
        <button type="button" class="flex min-h-10 w-full items-center gap-3 rounded-lg px-2.5 text-sm text-slate-700 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg class="size-4 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 4h4v16h-4M10 8l-4 4 4 4M6 12h10"/></svg>Sign out</button>
      </div>
    </div>
  </aside>
  <div class="hidden min-w-0 flex-1 p-8 md:block">
    <h2 class="text-lg font-semibold">Inbox</h2>
    <ul class="mt-4 divide-y divide-slate-200 rounded-xl bg-white text-sm ring-1 ring-slate-200">
      <li class="flex items-center gap-3 px-4 py-3"><span class="size-2 shrink-0 rounded-full bg-violet-600" aria-hidden="true"></span><span class="min-w-0 flex-1 truncate">Kai assigned you PLT-482, rate limit on export API</span><span class="shrink-0 text-slate-500">9m</span></li>
      <li class="flex items-center gap-3 px-4 py-3"><span class="size-2 shrink-0 rounded-full bg-violet-600" aria-hidden="true"></span><span class="min-w-0 flex-1 truncate">Design review moved to Thursday</span><span class="shrink-0 text-slate-500">1h</span></li>
      <li class="flex items-center gap-3 px-4 py-3"><span class="size-2 shrink-0 rounded-full bg-transparent" aria-hidden="true"></span><span class="min-w-0 flex-1 truncate">Growth closed GRW-77, onboarding checklist</span><span class="shrink-0 text-slate-500">3h</span></li>
    </ul>
  </div>
</div>

Sidebar with a live filter

  • type="search"
  • placeholder:
  • hidden attribute
  • aria-live
  • <mark>

Type in the filter, for example to or dia. Press / to jump to it.

The filter is a labelled search input styled with focus: and placeholder: utilities, and the slash key focuses it, as the kbd hint promises. The script hides non-matching items and empty sections with the hidden attribute, wraps the matched letters in a mark element built with createElement, shows an empty state and announces the count through an aria-live region. Escape clears the field.

View code
<nav aria-label="Documentation" class="w-full max-w-xs rounded-2xl border border-slate-200 bg-white p-3 text-slate-900 shadow-sm">
  <div class="relative">
    <label for="tsb-sf-input" class="sr-only">Filter pages</label>
    <svg class="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM20 20l-4-4"/></svg>
    <input id="tsb-sf-input" type="search" placeholder="Filter pages" autocomplete="off" class="h-11 w-full rounded-lg border border-slate-300 bg-slate-50 pr-10 pl-9 text-sm text-slate-900 placeholder:text-slate-500 focus:border-rose-500 focus:bg-white focus:outline-2 focus:outline-offset-0 focus:outline-rose-500/30 [&::-webkit-search-cancel-button]:hidden">
    <kbd class="pointer-events-none absolute top-1/2 right-2.5 -translate-y-1/2 rounded border border-slate-300 bg-white px-1.5 font-mono text-xs text-slate-500" aria-hidden="true">/</kbd>
  </div>
  <p id="tsb-sf-status" class="sr-only" aria-live="polite"></p>
    <div class="tsb-sf-group mt-4">
      <h3 id="tsb-sf-h0" class="px-2 pb-1 text-xs font-semibold tracking-wide text-slate-500 uppercase">Getting started</h3>
      <ul aria-labelledby="tsb-sf-h0">
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Introduction</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Installation</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Theming</span></a></li>
      </ul>
    </div>
    <div class="tsb-sf-group mt-4">
      <h3 id="tsb-sf-h1" class="px-2 pb-1 text-xs font-semibold tracking-wide text-slate-500 uppercase">Forms</h3>
      <ul aria-labelledby="tsb-sf-h1">
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Text input</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Select</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Checkbox</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Radio group</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Date picker</span></a></li>
      </ul>
    </div>
    <div class="tsb-sf-group mt-4">
      <h3 id="tsb-sf-h2" class="px-2 pb-1 text-xs font-semibold tracking-wide text-slate-500 uppercase">Overlays</h3>
      <ul aria-labelledby="tsb-sf-h2">
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Dialog</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Popover</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Tooltip</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Toast</span></a></li>
      </ul>
    </div>
    <div class="tsb-sf-group mt-4">
      <h3 id="tsb-sf-h3" class="px-2 pb-1 text-xs font-semibold tracking-wide text-slate-500 uppercase">Navigation</h3>
      <ul aria-labelledby="tsb-sf-h3">
        <li class="tsb-sf-item"><a href="#" aria-current="page" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Sidebar</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Tabs</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Breadcrumbs</span></a></li>
        <li class="tsb-sf-item"><a href="#" class="flex min-h-9 items-center rounded-md px-2 text-sm text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-rose-600 aria-[current=page]:bg-rose-50 aria-[current=page]:font-semibold aria-[current=page]:text-rose-800"><span class="tsb-sf-label">Pagination</span></a></li>
      </ul>
    </div>
  <p id="tsb-sf-empty" hidden class="mt-4 rounded-lg bg-slate-50 px-3 py-6 text-center text-sm text-slate-600">No pages match <span id="tsb-sf-term" class="font-semibold text-slate-900"></span></p>
</nav>

Sections styled by has-[[aria-current=page]]:

  • has-[[aria-current=page]]:
  • group-has-[[aria-current=page]]/sec:
  • aria-[current=page]:
  • border-l-2

Click a link in another section.

The current link only carries aria-current="page", and everything else follows from it in CSS. has-[[aria-current=page]]: tints the section that contains it, group-has-[[aria-current=page]]/sec: recolors that section heading, icon tile and rail line and reveals a You are here pill, and aria-[current=page]: marks the link itself. The script only moves the attribute when you click, so there is no second piece of state to keep in sync.

View code
<nav aria-label="Account" class="w-full max-w-xs rounded-2xl bg-white p-3 text-stone-900 shadow-sm ring-1 ring-stone-200">
  <ul class="space-y-2">
    <li class="group/sec rounded-xl border border-transparent p-2 has-[[aria-current=page]]:border-orange-200 has-[[aria-current=page]]:bg-orange-50">
      <h3 id="tsb-hc-bank" class="flex items-center gap-2.5 px-1 py-1.5 text-sm font-semibold text-stone-600 group-has-[[aria-current=page]]/sec:text-orange-900">
        <span class="grid size-7 place-items-center rounded-md bg-stone-100 text-stone-500 group-has-[[aria-current=page]]/sec:bg-orange-600 group-has-[[aria-current=page]]/sec:text-white"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v18M16.5 7.5c-.8-1.2-2.4-2-4.5-2-2.5 0-4 1.2-4 3s1.6 2.6 4 3.2 4 1.4 4 3.3-1.6 3.2-4.2 3.2c-2.1 0-3.8-.8-4.6-2.2"/></svg></span>
        Banking
        <span class="ml-auto hidden rounded-full bg-orange-600 px-2 py-0.5 text-[11px] font-semibold text-white group-has-[[aria-current=page]]/sec:inline">You are here</span>
      </h3>
      <ul aria-labelledby="tsb-hc-bank" class="mt-1 ml-[1.375rem] border-l border-stone-200 group-has-[[aria-current=page]]/sec:border-orange-200">
          <li><a href="#" class="tsb-hc-link -ml-px flex min-h-9 items-center border-l-2 border-transparent pl-4 text-sm text-stone-600 hover:border-stone-400 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 aria-[current=page]:border-orange-600 aria-[current=page]:font-semibold aria-[current=page]:text-orange-900">Accounts</a></li>
          <li><a href="#" class="tsb-hc-link -ml-px flex min-h-9 items-center border-l-2 border-transparent pl-4 text-sm text-stone-600 hover:border-stone-400 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 aria-[current=page]:border-orange-600 aria-[current=page]:font-semibold aria-[current=page]:text-orange-900">Transfers</a></li>
          <li><a href="#" class="tsb-hc-link -ml-px flex min-h-9 items-center border-l-2 border-transparent pl-4 text-sm text-stone-600 hover:border-stone-400 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 aria-[current=page]:border-orange-600 aria-[current=page]:font-semibold aria-[current=page]:text-orange-900">Statements</a></li>
      </ul>
    </li>
    <li class="group/sec rounded-xl border border-transparent p-2 has-[[aria-current=page]]:border-orange-200 has-[[aria-current=page]]:bg-orange-50">
      <h3 id="tsb-hc-spend" class="flex items-center gap-2.5 px-1 py-1.5 text-sm font-semibold text-stone-600 group-has-[[aria-current=page]]/sec:text-orange-900">
        <span class="grid size-7 place-items-center rounded-md bg-stone-100 text-stone-500 group-has-[[aria-current=page]]/sec:bg-orange-600 group-has-[[aria-current=page]]/sec:text-white"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18v12H3zM3 10h18M7 15h4"/></svg></span>
        Spending
        <span class="ml-auto hidden rounded-full bg-orange-600 px-2 py-0.5 text-[11px] font-semibold text-white group-has-[[aria-current=page]]/sec:inline">You are here</span>
      </h3>
      <ul aria-labelledby="tsb-hc-spend" class="mt-1 ml-[1.375rem] border-l border-stone-200 group-has-[[aria-current=page]]/sec:border-orange-200">
          <li><a href="#" class="tsb-hc-link -ml-px flex min-h-9 items-center border-l-2 border-transparent pl-4 text-sm text-stone-600 hover:border-stone-400 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 aria-[current=page]:border-orange-600 aria-[current=page]:font-semibold aria-[current=page]:text-orange-900">Cards</a></li>
          <li><a href="#" aria-current="page" class="tsb-hc-link -ml-px flex min-h-9 items-center border-l-2 border-transparent pl-4 text-sm text-stone-600 hover:border-stone-400 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 aria-[current=page]:border-orange-600 aria-[current=page]:font-semibold aria-[current=page]:text-orange-900">Budgets</a></li>
          <li><a href="#" class="tsb-hc-link -ml-px flex min-h-9 items-center border-l-2 border-transparent pl-4 text-sm text-stone-600 hover:border-stone-400 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 aria-[current=page]:border-orange-600 aria-[current=page]:font-semibold aria-[current=page]:text-orange-900">Subscriptions</a></li>
      </ul>
    </li>
    <li class="group/sec rounded-xl border border-transparent p-2 has-[[aria-current=page]]:border-orange-200 has-[[aria-current=page]]:bg-orange-50">
      <h3 id="tsb-hc-plan" class="flex items-center gap-2.5 px-1 py-1.5 text-sm font-semibold text-stone-600 group-has-[[aria-current=page]]/sec:text-orange-900">
        <span class="grid size-7 place-items-center rounded-md bg-stone-100 text-stone-500 group-has-[[aria-current=page]]/sec:bg-orange-600 group-has-[[aria-current=page]]/sec:text-white"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 20V11M12 20V5M19 20v-6"/></svg></span>
        Planning
        <span class="ml-auto hidden rounded-full bg-orange-600 px-2 py-0.5 text-[11px] font-semibold text-white group-has-[[aria-current=page]]/sec:inline">You are here</span>
      </h3>
      <ul aria-labelledby="tsb-hc-plan" class="mt-1 ml-[1.375rem] border-l border-stone-200 group-has-[[aria-current=page]]/sec:border-orange-200">
          <li><a href="#" class="tsb-hc-link -ml-px flex min-h-9 items-center border-l-2 border-transparent pl-4 text-sm text-stone-600 hover:border-stone-400 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 aria-[current=page]:border-orange-600 aria-[current=page]:font-semibold aria-[current=page]:text-orange-900">Goals</a></li>
          <li><a href="#" class="tsb-hc-link -ml-px flex min-h-9 items-center border-l-2 border-transparent pl-4 text-sm text-stone-600 hover:border-stone-400 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 aria-[current=page]:border-orange-600 aria-[current=page]:font-semibold aria-[current=page]:text-orange-900">Forecasts</a></li>
          <li><a href="#" class="tsb-hc-link -ml-px flex min-h-9 items-center border-l-2 border-transparent pl-4 text-sm text-stone-600 hover:border-stone-400 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 aria-[current=page]:border-orange-600 aria-[current=page]:font-semibold aria-[current=page]:text-orange-900">Tax documents</a></li>
      </ul>
    </li>
  </ul>
</nav>

Icon rail with popover flyout submenus

  • popover
  • popovertarget
  • aria-expanded:
  • data-active:
  • overflow-y-auto

Click an icon in the dark rail.

The rail scrolls with overflow-y-auto, which would clip any flyout positioned inside it. Each submenu is a native popover instead, so it renders in the top layer above every overflow and stacking context, closes on Escape or an outside click, and only one auto popover stays open at a time. The script places the open flyout beside its button and mirrors the state in aria-expanded, which the aria-expanded: variant uses to invert the button colors; data-active: marks the section that holds the current page.

View code
<div class="flex min-h-[460px] w-full bg-slate-50 text-slate-900">
  <nav aria-label="Main" class="flex w-16 shrink-0 flex-col items-center gap-4 overflow-y-auto bg-indigo-950 py-4">
    <span class="grid size-10 place-items-center rounded-xl bg-indigo-500 text-white" aria-hidden="true"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M13 3L5 14h6l-1 7 8-11h-6z"/></svg></span>
    <ul class="flex flex-col items-center gap-2">
      <li>
        <button type="button" popovertarget="tsb-fly-sales" aria-expanded="false" data-active class="tsb-fly-btn relative grid size-11 place-items-center rounded-xl text-indigo-200 hover:bg-indigo-900 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white aria-expanded:bg-white aria-expanded:text-indigo-950 data-active:bg-indigo-800 data-active:text-white aria-expanded:data-active:bg-white aria-expanded:data-active:text-indigo-950">
          <span class="sr-only">Sales, current section</span>
          <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v18M16.5 7.5c-.8-1.2-2.4-2-4.5-2-2.5 0-4 1.2-4 3s1.6 2.6 4 3.2 4 1.4 4 3.3-1.6 3.2-4.2 3.2c-2.1 0-3.8-.8-4.6-2.2"/></svg>
        </button>
      </li>
      <li>
        <button type="button" popovertarget="tsb-fly-cust" aria-expanded="false" class="tsb-fly-btn relative grid size-11 place-items-center rounded-xl text-indigo-200 hover:bg-indigo-900 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white aria-expanded:bg-white aria-expanded:text-indigo-950 data-active:bg-indigo-800 data-active:text-white aria-expanded:data-active:bg-white aria-expanded:data-active:text-indigo-950">
          <span class="sr-only">Customers</span>
          <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 19c0-2.8-2.2-5-5-5s-5 2.2-5 5M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM16 11a3 3 0 0 0 0-6M20 19c0-2.4-1.4-4.3-3.4-4.8"/></svg>
        </button>
      </li>
      <li>
        <button type="button" popovertarget="tsb-fly-rep" aria-expanded="false" class="tsb-fly-btn relative grid size-11 place-items-center rounded-xl text-indigo-200 hover:bg-indigo-900 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white aria-expanded:bg-white aria-expanded:text-indigo-950 data-active:bg-indigo-800 data-active:text-white aria-expanded:data-active:bg-white aria-expanded:data-active:text-indigo-950">
          <span class="sr-only">Reports</span>
          <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 20V11M12 20V5M19 20v-6"/></svg>
        </button>
      </li>
      <li>
        <button type="button" popovertarget="tsb-fly-cal" aria-expanded="false" class="tsb-fly-btn relative grid size-11 place-items-center rounded-xl text-indigo-200 hover:bg-indigo-900 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white aria-expanded:bg-white aria-expanded:text-indigo-950 data-active:bg-indigo-800 data-active:text-white aria-expanded:data-active:bg-white aria-expanded:data-active:text-indigo-950">
          <span class="sr-only">Calendar</span>
          <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16v14H4zM4 10h16M8 3v4M16 3v4"/></svg>
        </button>
      </li>
      <li>
        <button type="button" popovertarget="tsb-fly-set" aria-expanded="false" class="tsb-fly-btn relative grid size-11 place-items-center rounded-xl text-indigo-200 hover:bg-indigo-900 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white aria-expanded:bg-white aria-expanded:text-indigo-950 data-active:bg-indigo-800 data-active:text-white aria-expanded:data-active:bg-white aria-expanded:data-active:text-indigo-950">
          <span class="sr-only">Settings</span>
          <svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2z"/></svg>
        </button>
      </li>
    </ul>
  </nav>
  <div class="min-w-0 flex-1 p-4 md:p-8">
    <h2 class="text-lg font-semibold">Pipeline</h2>
    <p class="mt-1 text-sm text-slate-600">31 open deals worth $412,800.</p>
    <div class="mt-6 grid gap-3 md:grid-cols-3">
      <div class="rounded-xl bg-white p-4 ring-1 ring-slate-200"><p class="text-sm font-semibold">Qualified</p><p class="mt-1 text-sm text-slate-600">12 deals, $96,400</p></div>
      <div class="rounded-xl bg-white p-4 ring-1 ring-slate-200"><p class="text-sm font-semibold">Proposal</p><p class="mt-1 text-sm text-slate-600">11 deals, $188,900</p></div>
      <div class="rounded-xl bg-white p-4 ring-1 ring-slate-200"><p class="text-sm font-semibold">Negotiation</p><p class="mt-1 text-sm text-slate-600">8 deals, $127,500</p></div>
    </div>
  </div>
</div>
<div id="tsb-fly-sales" popover class="tsb-fly inset-auto m-0 w-56 rounded-xl border border-slate-200 bg-white p-2 text-slate-900 shadow-xl">
  <h3 class="px-3 pt-1 pb-2 text-sm font-semibold">Sales</h3>
  <ul>
    <li><a href="#" aria-current="page" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Pipeline</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Deals</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Quotes</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Forecast</a></li>
  </ul>
</div>
<div id="tsb-fly-cust" popover class="tsb-fly inset-auto m-0 w-56 rounded-xl border border-slate-200 bg-white p-2 text-slate-900 shadow-xl">
  <h3 class="px-3 pt-1 pb-2 text-sm font-semibold">Customers</h3>
  <ul>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Accounts</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Contacts</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Segments</a></li>
  </ul>
</div>
<div id="tsb-fly-rep" popover class="tsb-fly inset-auto m-0 w-56 rounded-xl border border-slate-200 bg-white p-2 text-slate-900 shadow-xl">
  <h3 class="px-3 pt-1 pb-2 text-sm font-semibold">Reports</h3>
  <ul>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Revenue</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Activity</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Win rate</a></li>
  </ul>
</div>
<div id="tsb-fly-cal" popover class="tsb-fly inset-auto m-0 w-56 rounded-xl border border-slate-200 bg-white p-2 text-slate-900 shadow-xl">
  <h3 class="px-3 pt-1 pb-2 text-sm font-semibold">Calendar</h3>
  <ul>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">My week</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Team calendar</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Booking pages</a></li>
  </ul>
</div>
<div id="tsb-fly-set" popover class="tsb-fly inset-auto m-0 w-56 rounded-xl border border-slate-200 bg-white p-2 text-slate-900 shadow-xl">
  <h3 class="px-3 pt-1 pb-2 text-sm font-semibold">Settings</h3>
  <ul>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Team</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Pipelines</a></li>
    <li><a href="#" class="flex min-h-10 items-center rounded-lg px-3 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-900 focus-visible:outline-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-indigo-50 aria-[current=page]:font-semibold aria-[current=page]:text-indigo-900">Integrations</a></li>
  </ul>
</div>

Sidebars for real apps

Five product sidebars with their own palettes: a dark analytics panel, settings with secondary navigation, mail folders, a workspace switcher on the Popover API and a chat channel list.

Dark mode sidebar with light pairs

  • dark:bg-gray-900
  • dark:aria-[current=page]:
  • dark:focus-visible:outline-lime-400
  • role="meter"
  • kbd

Every color has a light utility and a dark: partner, the same pairs as the table in the guide: bg-white and dark:bg-gray-900 for the panel, gray-200 and dark:gray-800 for borders, and dark:aria-[current=page]:bg-gray-700 for the active row. The focus ring changes too, from indigo on light to lime-400 on dark, because a ring that works on white can vanish on gray-900. The stage puts .dark on the html element, which is what the @custom-variant line in the setup box matches.

View code
<aside aria-label="Sidebar" class="flex w-full max-w-xs flex-col gap-5 rounded-2xl border border-gray-200 bg-white p-4 text-gray-900 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-white">
  <div class="flex items-center gap-2.5 px-1">
    <span class="grid size-9 place-items-center rounded-lg bg-indigo-600 text-white dark:bg-lime-400 dark:text-gray-950"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3zM5 11a7 7 0 0 0 14 0M12 18v3"/></svg></span>
    <span class="text-base font-semibold">Waveform</span>
    <button type="button" class="ml-auto flex h-9 items-center gap-2 rounded-lg border border-gray-200 px-2.5 text-xs text-gray-600 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-indigo-600 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:focus-visible:outline-lime-400"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM20 20l-4-4"/></svg><span>Search</span><kbd class="font-sans text-gray-500 dark:text-gray-400">Ctrl K</kbd></button>
  </div>
  <nav aria-label="Main">
    <ul class="space-y-0.5">
      <li><a href="#" aria-current="page" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-gray-200 aria-[current=page]:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:outline-lime-400 dark:aria-[current=page]:bg-gray-700 dark:aria-[current=page]:text-white"><svg class="size-5 shrink-0 text-gray-400 group-aria-[current=page]:text-indigo-600 dark:text-gray-500 dark:group-aria-[current=page]:text-lime-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 11l9-7 9 7M5 9.5V20h5v-6h4v6h5V9.5"/></svg><span class="truncate">Overview</span></a></li>
      <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-gray-200 aria-[current=page]:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:outline-lime-400 dark:aria-[current=page]:bg-gray-700 dark:aria-[current=page]:text-white"><svg class="size-5 shrink-0 text-gray-400 group-aria-[current=page]:text-indigo-600 dark:text-gray-500 dark:group-aria-[current=page]:text-lime-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 4l13 8-13 8z"/></svg><span class="truncate">Episodes</span><span class="ml-auto rounded-full bg-indigo-50 px-2 py-0.5 text-xs font-semibold text-indigo-700 dark:bg-lime-400/15 dark:text-lime-300">3<span class="sr-only"> drafts</span></span></a></li>
      <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-gray-200 aria-[current=page]:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:outline-lime-400 dark:aria-[current=page]:bg-gray-700 dark:aria-[current=page]:text-white"><svg class="size-5 shrink-0 text-gray-400 group-aria-[current=page]:text-indigo-600 dark:text-gray-500 dark:group-aria-[current=page]:text-lime-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 19c0-2.8-2.2-5-5-5s-5 2.2-5 5M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM16 11a3 3 0 0 0 0-6M20 19c0-2.4-1.4-4.3-3.4-4.8"/></svg><span class="truncate">Audience</span></a></li>
      <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-gray-200 aria-[current=page]:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:outline-lime-400 dark:aria-[current=page]:bg-gray-700 dark:aria-[current=page]:text-white"><svg class="size-5 shrink-0 text-gray-400 group-aria-[current=page]:text-indigo-600 dark:text-gray-500 dark:group-aria-[current=page]:text-lime-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v18M16.5 7.5c-.8-1.2-2.4-2-4.5-2-2.5 0-4 1.2-4 3s1.6 2.6 4 3.2 4 1.4 4 3.3-1.6 3.2-4.2 3.2c-2.1 0-3.8-.8-4.6-2.2"/></svg><span class="truncate">Monetization</span></a></li>
      <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-gray-200 aria-[current=page]:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:outline-lime-400 dark:aria-[current=page]:bg-gray-700 dark:aria-[current=page]:text-white"><svg class="size-5 shrink-0 text-gray-400 group-aria-[current=page]:text-indigo-600 dark:text-gray-500 dark:group-aria-[current=page]:text-lime-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM8.1 7.9L20 20M8.1 16.1L20 4"/></svg><span class="truncate">Clips</span></a></li>
    </ul>
  </nav>
  <div>
    <h3 id="tsb-dk-shows" class="px-3 pb-2 text-xs font-semibold tracking-wide text-gray-500 uppercase dark:text-gray-400">Shows</h3>
    <ul aria-labelledby="tsb-dk-shows" class="space-y-0.5">
      <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-gray-200 aria-[current=page]:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:outline-lime-400 dark:aria-[current=page]:bg-gray-700 dark:aria-[current=page]:text-white"><span class="grid size-6 shrink-0 place-items-center rounded-md bg-amber-300 text-[11px] font-bold text-gray-950" aria-hidden="true">N</span><span class="truncate">Night Shift Radio</span><span class="ml-auto text-xs text-gray-500 dark:text-gray-400">112<span class="sr-only"> episodes</span></span></a></li>
      <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-gray-200 aria-[current=page]:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:outline-lime-400 dark:aria-[current=page]:bg-gray-700 dark:aria-[current=page]:text-white"><span class="grid size-6 shrink-0 place-items-center rounded-md bg-sky-300 text-[11px] font-bold text-gray-950" aria-hidden="true">S</span><span class="truncate">Small Batch</span><span class="ml-auto text-xs text-gray-500 dark:text-gray-400">48<span class="sr-only"> episodes</span></span></a></li>
      <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 aria-[current=page]:bg-gray-200 aria-[current=page]:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white dark:focus-visible:outline-lime-400 dark:aria-[current=page]:bg-gray-700 dark:aria-[current=page]:text-white"><span class="grid size-6 shrink-0 place-items-center rounded-md bg-pink-300 text-[11px] font-bold text-gray-950" aria-hidden="true">F</span><span class="truncate">Field Notes</span><span class="ml-auto text-xs text-gray-500 dark:text-gray-400">23<span class="sr-only"> episodes</span></span></a></li>
    </ul>
  </div>
  <div class="rounded-xl border border-gray-200 p-3 dark:border-gray-800">
    <div class="flex items-center justify-between text-xs"><span id="tsb-dk-storage" class="font-medium text-gray-700 dark:text-gray-300">Storage</span><span class="text-gray-500 dark:text-gray-400">18.4 of 25 GB</span></div>
    <div role="meter" aria-labelledby="tsb-dk-storage" aria-valuemin="0" aria-valuemax="25" aria-valuenow="18.4" aria-valuetext="18.4 of 25 GB" class="mt-2 h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800">
      <div class="h-full w-[74%] rounded-full bg-indigo-600 dark:bg-lime-400"></div>
    </div>
  </div>
</aside>

Settings page with secondary navigation

  • md:grid-cols-[14rem_1fr]
  • border-l-2
  • aria-[current=page]:border-cyan-700
  • md:hidden
  • accent-*

Pick a section from the list, or from the select on a phone.

A two-column grid with md:grid-cols-[14rem_1fr] holds the secondary nav and the panel, and the current item gets a left bar from border-l-2 plus aria-[current=page]:border-cyan-700. Below 768px the list is replaced by a native select, which fits six destinations in one row of height and still works with the keyboard. The script keeps the links, the select and the visible section in sync and moves focus to the section heading after a click.

View code
<div class="w-full bg-slate-50 text-slate-900">
  <div class="border-b border-slate-200 bg-white px-4 py-5 md:px-8">
    <h2 class="text-xl font-semibold">Settings</h2>
    <p class="mt-1 text-sm text-slate-600">Manage your account and the Orbit Studio workspace.</p>
  </div>
  <div class="px-4 py-6 md:grid md:grid-cols-[14rem_1fr] md:gap-10 md:px-8">
    <div class="relative mb-6 md:hidden">
      <label for="tsb-set-select" class="text-sm font-medium">Section</label>
      <select id="tsb-set-select" class="mt-1.5 h-11 w-full appearance-none rounded-lg border border-slate-300 bg-white pr-10 pl-3 text-sm text-slate-900 focus:border-cyan-700 focus:outline-2 focus:outline-cyan-700/20">
          <option value="tsb-set-profile">Profile</option>
          <option value="tsb-set-security">Security</option>
          <option value="tsb-set-notify">Notifications</option>
          <option value="tsb-set-general">General</option>
          <option value="tsb-set-billing">Billing</option>
          <option value="tsb-set-members">Members</option>
      </select>
      <svg class="pointer-events-none absolute right-3 bottom-3.5 size-4 text-slate-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
    </div>
    <nav aria-label="Settings" class="hidden md:block">
      <h3 id="tsb-set-g0" class="pb-2 text-xs font-semibold tracking-wide text-slate-500 uppercase">Account</h3>
      <ul aria-labelledby="tsb-set-g0" class="space-y-0.5">
          <li><a href="#tsb-set-profile" data-panel="tsb-set-profile" aria-current="page" class="tsb-set-link flex min-h-10 items-center gap-3 rounded-r-md border-l-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-cyan-700 aria-[current=page]:border-cyan-700 aria-[current=page]:bg-cyan-50 aria-[current=page]:text-cyan-900"><svg class="size-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM4 21c0-4 3.6-7 8-7s8 3 8 7"/></svg>Profile</a></li>
          <li><a href="#tsb-set-security" data-panel="tsb-set-security" class="tsb-set-link flex min-h-10 items-center gap-3 rounded-r-md border-l-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-cyan-700 aria-[current=page]:border-cyan-700 aria-[current=page]:bg-cyan-50 aria-[current=page]:text-cyan-900"><svg class="size-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l8 3v6c0 4.5-3.4 8.3-8 9-4.6-.7-8-4.5-8-9V6z"/></svg>Security</a></li>
          <li><a href="#tsb-set-notify" data-panel="tsb-set-notify" class="tsb-set-link flex min-h-10 items-center gap-3 rounded-r-md border-l-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-cyan-700 aria-[current=page]:border-cyan-700 aria-[current=page]:bg-cyan-50 aria-[current=page]:text-cyan-900"><svg class="size-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 16v-5a6 6 0 1 0-12 0v5l-2 2h16zM10 21h4"/></svg>Notifications</a></li>
      </ul>
      <h3 id="tsb-set-g1" class="mt-6 pb-2 text-xs font-semibold tracking-wide text-slate-500 uppercase">Workspace</h3>
      <ul aria-labelledby="tsb-set-g1" class="space-y-0.5">
          <li><a href="#tsb-set-general" data-panel="tsb-set-general" class="tsb-set-link flex min-h-10 items-center gap-3 rounded-r-md border-l-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-cyan-700 aria-[current=page]:border-cyan-700 aria-[current=page]:bg-cyan-50 aria-[current=page]:text-cyan-900"><svg class="size-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2z"/></svg>General</a></li>
          <li><a href="#tsb-set-billing" data-panel="tsb-set-billing" class="tsb-set-link flex min-h-10 items-center gap-3 rounded-r-md border-l-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-cyan-700 aria-[current=page]:border-cyan-700 aria-[current=page]:bg-cyan-50 aria-[current=page]:text-cyan-900"><svg class="size-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18v12H3zM3 10h18M7 15h4"/></svg>Billing</a></li>
          <li><a href="#tsb-set-members" data-panel="tsb-set-members" class="tsb-set-link flex min-h-10 items-center gap-3 rounded-r-md border-l-2 border-transparent px-3 text-sm font-medium text-slate-600 hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-cyan-700 aria-[current=page]:border-cyan-700 aria-[current=page]:bg-cyan-50 aria-[current=page]:text-cyan-900"><svg class="size-5 shrink-0 text-slate-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 19c0-2.8-2.2-5-5-5s-5 2.2-5 5M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM16 11a3 3 0 0 0 0-6M20 19c0-2.4-1.4-4.3-3.4-4.8"/></svg>Members</a></li>
      </ul>
    </nav>
    <div class="min-w-0">
    <section id="tsb-set-profile" class="tsb-set-panel" aria-labelledby="tsb-set-profile-h">
      <h3 id="tsb-set-profile-h" tabindex="-1" class="text-base font-semibold focus:outline-none">Profile</h3>
      <p class="mt-1 text-sm text-slate-600">How your name and photo appear to the rest of the workspace.</p>
      <div class="mt-5 divide-y divide-slate-200 rounded-xl border border-slate-200 bg-white">
        <div class="px-4 py-4 md:px-5"><label for="tsb-set-name" class="block text-sm font-medium">Display name</label><input id="tsb-set-name" type="text" value="Ana Kowalski" class="mt-1.5 h-11 w-full rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-900 focus:border-cyan-700 focus:outline-2 focus:outline-cyan-700/20 md:max-w-sm"></div>
        <div class="px-4 py-4 md:px-5"><label for="tsb-set-email" class="block text-sm font-medium">Email</label><input id="tsb-set-email" type="email" value="ana@orbit.example" class="mt-1.5 h-11 w-full rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-900 focus:border-cyan-700 focus:outline-2 focus:outline-cyan-700/20 md:max-w-sm"></div>
      </div>
    </section>
    <section id="tsb-set-security" class="tsb-set-panel" hidden aria-labelledby="tsb-set-security-h">
      <h3 id="tsb-set-security-h" tabindex="-1" class="text-base font-semibold focus:outline-none">Security</h3>
      <p class="mt-1 text-sm text-slate-600">Password, two-step sign in and the devices that are signed in.</p>
      <div class="mt-5 divide-y divide-slate-200 rounded-xl border border-slate-200 bg-white">
        <div class="flex flex-col gap-2 px-4 py-4 md:flex-row md:items-center md:justify-between md:px-5"><div><p class="text-sm font-medium">Password</p><p class="text-sm text-slate-500">Last changed 41 days ago</p></div><button type="button" class="inline-flex min-h-10 items-center justify-center rounded-lg border border-slate-300 bg-white px-3.5 text-sm font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-700">Change</button></div>
        <div class="flex flex-col gap-2 px-4 py-4 md:flex-row md:items-center md:justify-between md:px-5"><div><p class="text-sm font-medium">Two-step sign in</p><p class="text-sm text-slate-500">Authenticator app, on</p></div><button type="button" class="inline-flex min-h-10 items-center justify-center rounded-lg border border-slate-300 bg-white px-3.5 text-sm font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-700">Manage</button></div>
      </div>
    </section>
    <section id="tsb-set-notify" class="tsb-set-panel" hidden aria-labelledby="tsb-set-notify-h">
      <h3 id="tsb-set-notify-h" tabindex="-1" class="text-base font-semibold focus:outline-none">Notifications</h3>
      <p class="mt-1 text-sm text-slate-600">Choose what reaches your inbox. Mentions always notify you in the app.</p>
      <div class="mt-5 divide-y divide-slate-200 rounded-xl border border-slate-200 bg-white">
        <label class="flex items-start gap-3 px-4 py-4 md:px-5"><input type="checkbox" checked class="mt-0.5 size-5 shrink-0 accent-cyan-700"><span><span class="block text-sm font-medium">Weekly summary</span><span class="block text-sm text-slate-500">Every Monday at 8 AM</span></span></label>
        <label class="flex items-start gap-3 px-4 py-4 md:px-5"><input type="checkbox" class="mt-0.5 size-5 shrink-0 accent-cyan-700"><span><span class="block text-sm font-medium">New comments</span><span class="block text-sm text-slate-500">On tasks you follow</span></span></label>
      </div>
    </section>
    <section id="tsb-set-general" class="tsb-set-panel" hidden aria-labelledby="tsb-set-general-h">
      <h3 id="tsb-set-general-h" tabindex="-1" class="text-base font-semibold focus:outline-none">General</h3>
      <p class="mt-1 text-sm text-slate-600">The workspace name and address everyone shares.</p>
      <div class="mt-5 divide-y divide-slate-200 rounded-xl border border-slate-200 bg-white">
        <div class="px-4 py-4 md:px-5"><label for="tsb-set-ws" class="block text-sm font-medium">Workspace name</label><input id="tsb-set-ws" type="text" value="Orbit Studio" class="mt-1.5 h-11 w-full rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-900 focus:border-cyan-700 focus:outline-2 focus:outline-cyan-700/20 md:max-w-sm"></div>
        <div class="flex flex-col gap-2 px-4 py-4 md:flex-row md:items-center md:justify-between md:px-5"><div><p class="text-sm font-medium">Address</p><p class="text-sm text-slate-500">orbit.example/studio</p></div><button type="button" class="inline-flex min-h-10 items-center justify-center rounded-lg border border-slate-300 bg-white px-3.5 text-sm font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-700">Edit</button></div>
      </div>
    </section>
    <section id="tsb-set-billing" class="tsb-set-panel" hidden aria-labelledby="tsb-set-billing-h">
      <h3 id="tsb-set-billing-h" tabindex="-1" class="text-base font-semibold focus:outline-none">Billing</h3>
      <p class="mt-1 text-sm text-slate-600">Your plan, payment method and invoices.</p>
      <div class="mt-5 divide-y divide-slate-200 rounded-xl border border-slate-200 bg-white">
        <div class="flex flex-col gap-2 px-4 py-4 md:flex-row md:items-center md:justify-between md:px-5"><div><p class="text-sm font-medium">Team plan</p><p class="text-sm text-slate-500">$48 per month, 8 seats</p></div><button type="button" class="inline-flex min-h-10 items-center justify-center rounded-lg border border-slate-300 bg-white px-3.5 text-sm font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-700">Change plan</button></div>
        <div class="flex flex-col gap-2 px-4 py-4 md:flex-row md:items-center md:justify-between md:px-5"><div><p class="text-sm font-medium">Next invoice</p><p class="text-sm text-slate-500">November 14, 2026</p></div><button type="button" class="inline-flex min-h-10 items-center justify-center rounded-lg border border-slate-300 bg-white px-3.5 text-sm font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-700">View invoices</button></div>
      </div>
    </section>
    <section id="tsb-set-members" class="tsb-set-panel" hidden aria-labelledby="tsb-set-members-h">
      <h3 id="tsb-set-members-h" tabindex="-1" class="text-base font-semibold focus:outline-none">Members</h3>
      <p class="mt-1 text-sm text-slate-600">Eight people have access to this workspace.</p>
      <div class="mt-5 divide-y divide-slate-200 rounded-xl border border-slate-200 bg-white">
        <div class="flex items-center gap-3 px-4 py-3 md:px-5"><span class="grid size-9 shrink-0 place-items-center rounded-full bg-cyan-100 text-xs font-bold text-cyan-900" aria-hidden="true">AK</span><span class="min-w-0 flex-1 truncate text-sm font-medium">Ana Kowalski</span><span class="text-sm text-slate-500">Owner</span></div>
        <div class="flex items-center gap-3 px-4 py-3 md:px-5"><span class="grid size-9 shrink-0 place-items-center rounded-full bg-amber-100 text-xs font-bold text-amber-900" aria-hidden="true">DO</span><span class="min-w-0 flex-1 truncate text-sm font-medium">Dele Okoro</span><span class="text-sm text-slate-500">Admin</span></div>
        <div class="flex items-center gap-3 px-4 py-3 md:px-5"><span class="grid size-9 shrink-0 place-items-center rounded-full bg-violet-100 text-xs font-bold text-violet-900" aria-hidden="true">SL</span><span class="min-w-0 flex-1 truncate text-sm font-medium">Sofia Lind</span><span class="text-sm text-slate-500">Member</span></div>
      </div>
    </section>
    </div>
  </div>
</div>

Mail folders with counts and labels

  • flex-wrap md:flex-col
  • rounded-full md:rounded-lg
  • group-aria-[current=page]:
  • md:ml-auto

Pick a folder.

One list changes shape with the breakpoint: flex flex-wrap with rounded-full chips on phones, then md:flex-col with md:rounded-lg rows and md:ml-auto pushing the counts to the right. Icons and the labels list only appear from md:, which keeps the chip row short. Counts carry sr-only words such as unread so they make sense out of context, and the script moves aria-current and swaps the message list for an empty state.

View code
<div class="w-full bg-slate-50 text-slate-900 md:flex md:min-h-[520px]">
  <aside aria-label="Mail" class="border-b border-slate-200 bg-white p-4 md:w-60 md:shrink-0 md:border-r md:border-b-0">
    <button type="button" class="flex h-11 w-full items-center justify-center gap-2 rounded-xl bg-blue-600 px-4 text-sm font-semibold text-white shadow-xs hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 md:justify-start"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4"/></svg>Compose</button>
    <nav aria-label="Folders" class="mt-4">
      <ul class="flex flex-wrap gap-1.5 md:flex-col md:gap-0.5">
        <li><a href="#" aria-current="page" class="tsb-mail-link group flex min-h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-[current=page]:border-blue-100 aria-[current=page]:bg-blue-100 aria-[current=page]:text-blue-900 md:gap-3 md:rounded-lg md:border-0 md:bg-transparent md:px-3"><svg class="hidden size-5 shrink-0 text-slate-500 group-aria-[current=page]:text-blue-700 md:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 13h5l1.5 3h5l1.5-3h5M5.5 5h13L21 13v6H3v-6z"/></svg>Inbox<span class="text-xs font-semibold text-slate-600 group-aria-[current=page]:text-blue-900 md:ml-auto">24<span class="sr-only"> unread</span></span></a></li>
        <li><a href="#" class="tsb-mail-link group flex min-h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-[current=page]:border-blue-100 aria-[current=page]:bg-blue-100 aria-[current=page]:text-blue-900 md:gap-3 md:rounded-lg md:border-0 md:bg-transparent md:px-3"><svg class="hidden size-5 shrink-0 text-slate-500 group-aria-[current=page]:text-blue-700 md:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3.5l2.6 5.3 5.9.9-4.3 4.1 1 5.8-5.2-2.8-5.2 2.8 1-5.8-4.3-4.1 5.9-.9z"/></svg>Starred</a></li>
        <li><a href="#" class="tsb-mail-link group flex min-h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-[current=page]:border-blue-100 aria-[current=page]:bg-blue-100 aria-[current=page]:text-blue-900 md:gap-3 md:rounded-lg md:border-0 md:bg-transparent md:px-3"><svg class="hidden size-5 shrink-0 text-slate-500 group-aria-[current=page]:text-blue-700 md:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18zM12 7v5l3 2"/></svg>Snoozed</a></li>
        <li><a href="#" class="tsb-mail-link group flex min-h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-[current=page]:border-blue-100 aria-[current=page]:bg-blue-100 aria-[current=page]:text-blue-900 md:gap-3 md:rounded-lg md:border-0 md:bg-transparent md:px-3"><svg class="hidden size-5 shrink-0 text-slate-500 group-aria-[current=page]:text-blue-700 md:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12l16-8-6 16-2.5-6.5z"/></svg>Sent</a></li>
        <li><a href="#" class="tsb-mail-link group flex min-h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-[current=page]:border-blue-100 aria-[current=page]:bg-blue-100 aria-[current=page]:text-blue-900 md:gap-3 md:rounded-lg md:border-0 md:bg-transparent md:px-3"><svg class="hidden size-5 shrink-0 text-slate-500 group-aria-[current=page]:text-blue-700 md:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3h8l4 4v14H6zM14 3v4h4"/></svg>Drafts<span class="text-xs font-semibold text-slate-600 group-aria-[current=page]:text-blue-900 md:ml-auto">2<span class="sr-only"> drafts</span></span></a></li>
        <li><a href="#" class="tsb-mail-link group flex min-h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-[current=page]:border-blue-100 aria-[current=page]:bg-blue-100 aria-[current=page]:text-blue-900 md:gap-3 md:rounded-lg md:border-0 md:bg-transparent md:px-3"><svg class="hidden size-5 shrink-0 text-slate-500 group-aria-[current=page]:text-blue-700 md:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h18v4H3zM5 9v10h14V9M10 13h4"/></svg>Archive</a></li>
        <li><a href="#" class="tsb-mail-link group flex min-h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-[current=page]:border-blue-100 aria-[current=page]:bg-blue-100 aria-[current=page]:text-blue-900 md:gap-3 md:rounded-lg md:border-0 md:bg-transparent md:px-3"><svg class="hidden size-5 shrink-0 text-slate-500 group-aria-[current=page]:text-blue-700 md:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4l9 16H3zM12 10v4M12 17h.01"/></svg>Spam</a></li>
        <li><a href="#" class="tsb-mail-link group flex min-h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 text-sm font-medium text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 aria-[current=page]:border-blue-100 aria-[current=page]:bg-blue-100 aria-[current=page]:text-blue-900 md:gap-3 md:rounded-lg md:border-0 md:bg-transparent md:px-3"><svg class="hidden size-5 shrink-0 text-slate-500 group-aria-[current=page]:text-blue-700 md:block" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></svg>Trash</a></li>
      </ul>
    </nav>
    <h3 id="tsb-mail-labels" class="mt-6 hidden px-3 pb-2 text-xs font-semibold tracking-wide text-slate-500 uppercase md:block">Labels</h3>
    <ul aria-labelledby="tsb-mail-labels" class="hidden space-y-0.5 md:block">
      <li><a href="#" class="flex min-h-9 items-center gap-3 rounded-lg px-3 text-sm text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-blue-600"><span class="size-2.5 rounded-full bg-emerald-500" aria-hidden="true"></span>Clients</a></li>
      <li><a href="#" class="flex min-h-9 items-center gap-3 rounded-lg px-3 text-sm text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-blue-600"><span class="size-2.5 rounded-full bg-amber-500" aria-hidden="true"></span>Invoices</a></li>
      <li><a href="#" class="flex min-h-9 items-center gap-3 rounded-lg px-3 text-sm text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-blue-600"><span class="size-2.5 rounded-full bg-fuchsia-500" aria-hidden="true"></span>Travel</a></li>
    </ul>
  </aside>
  <div class="min-w-0 flex-1 p-4 md:p-6">
    <h2 id="tsb-mail-title" class="text-lg font-semibold">Inbox</h2>
    <ul id="tsb-mail-list" class="mt-4 divide-y divide-slate-200 rounded-xl bg-white ring-1 ring-slate-200">
        <li class="flex gap-3 px-4 py-3.5">
          <span class="grid size-9 shrink-0 place-items-center rounded-full bg-emerald-100 text-emerald-900 text-xs font-bold" aria-hidden="true">PN</span>
          <div class="min-w-0 flex-1">
            <div class="flex items-baseline justify-between gap-3"><p class="truncate text-sm font-semibold">Priya Nair</p><span class="shrink-0 text-xs text-slate-500">9:42 AM</span></div>
            <p class="truncate text-sm font-semibold">Revised venue quote</p>
            <p class="truncate text-sm text-slate-500">The terrace option is back at the original price if we confirm by Friday.</p>
          </div>
        </li>
        <li class="flex gap-3 px-4 py-3.5">
          <span class="grid size-9 shrink-0 place-items-center rounded-full bg-amber-100 text-amber-900 text-xs font-bold" aria-hidden="true">HP</span>
          <div class="min-w-0 flex-1">
            <div class="flex items-baseline justify-between gap-3"><p class="truncate text-sm font-semibold">Harbor Print Co.</p><span class="shrink-0 text-xs text-slate-500">8:15 AM</span></div>
            <p class="truncate text-sm font-semibold">Proof ready for approval</p>
            <p class="truncate text-sm text-slate-500">Your 300 catalog proofs are ready. Reply with any changes.</p>
          </div>
        </li>
        <li class="flex gap-3 px-4 py-3.5">
          <span class="grid size-9 shrink-0 place-items-center rounded-full bg-sky-100 text-sky-900 text-xs font-bold" aria-hidden="true">TO</span>
          <div class="min-w-0 flex-1">
            <div class="flex items-baseline justify-between gap-3"><p class="truncate text-sm text-slate-700">Tom Okafor</p><span class="shrink-0 text-xs text-slate-500">Yesterday</span></div>
            <p class="truncate text-sm text-slate-700">Re: Q4 roadmap</p>
            <p class="truncate text-sm text-slate-500">Agree on moving search to January, the import work comes first.</p>
          </div>
        </li>
    </ul>
    <p id="tsb-mail-empty" hidden class="mt-4 rounded-xl border border-dashed border-slate-300 bg-white px-4 py-12 text-center text-sm text-slate-600">No conversations in <span id="tsb-mail-empty-name" class="font-semibold text-slate-900"></span>.</p>
  </div>
</div>

Workspace switcher on the Popover API

  • popover
  • aria-pressed:
  • group-aria-pressed:visible
  • hidePopover()
  • aria-expanded:

Open the workspace menu at the top of the sidebar and pick another workspace.

The switcher button opens a native popover with popovertarget, so light dismiss, Escape and focus return need no code. Options are buttons with aria-pressed, and aria-pressed: plus group-aria-pressed:visible tint the active row and show its check mark. The script places the popover under the button at the same width, swaps the logo color class (both class names are written out in the markup for Tailwind to find), updates the names and calls hidePopover().

View code
<div class="flex min-h-[520px] w-full bg-zinc-100 text-zinc-900">
  <aside aria-label="Sidebar" class="flex w-full flex-col gap-5 border-r border-zinc-200 bg-white p-3 md:w-64 md:shrink-0">
    <button type="button" id="tsb-ws-btn" popovertarget="tsb-ws-menu" aria-expanded="false" class="flex w-full items-center gap-3 rounded-xl border border-zinc-200 p-2 text-left hover:bg-zinc-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-600 aria-expanded:border-fuchsia-300 aria-expanded:bg-fuchsia-50">
      <span id="tsb-ws-logo" data-color="bg-fuchsia-600" class="grid size-9 shrink-0 place-items-center rounded-lg bg-fuchsia-600 text-sm font-bold text-white" aria-hidden="true">AS</span>
      <span class="min-w-0 flex-1"><span id="tsb-ws-name" class="block truncate text-sm font-semibold">Atlas Studio</span><span id="tsb-ws-plan" class="block truncate text-xs text-zinc-600">Pro plan, 12 members</span></span>
      <span class="sr-only">, switch workspace</span>
      <svg class="size-4 shrink-0 text-zinc-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 9l4-4 4 4M8 15l4 4 4-4"/></svg>
    </button>
    <div id="tsb-ws-menu" popover class="inset-auto m-0 w-60 rounded-xl border border-zinc-200 bg-white p-1.5 text-zinc-900 shadow-xl">
      <h3 class="px-2 pt-1.5 pb-1 text-xs font-medium text-zinc-600">Switch workspace</h3>
      <ul class="space-y-0.5">
        <li><button type="button" aria-pressed="true" data-name="Atlas Studio" data-plan="Pro plan, 12 members" data-color="bg-fuchsia-600" class="tsb-ws-opt group flex min-h-12 w-full items-center gap-3 rounded-lg px-2 text-left hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-fuchsia-600 aria-pressed:bg-fuchsia-50">
          <span class="tsb-ws-opt-logo grid size-8 shrink-0 place-items-center rounded-lg bg-fuchsia-600 text-xs font-bold text-white" aria-hidden="true">AS</span>
          <span class="min-w-0 flex-1"><span class="block truncate text-sm font-semibold text-zinc-900">Atlas Studio</span><span class="block truncate text-xs text-zinc-600">Pro plan, 12 members</span></span>
          <svg class="invisible size-5 shrink-0 text-fuchsia-700 group-aria-pressed:visible" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"/></svg>
        </button></li>
        <li><button type="button" aria-pressed="false" data-name="Mossbank Records" data-plan="Free plan, 3 members" data-color="bg-emerald-600" class="tsb-ws-opt group flex min-h-12 w-full items-center gap-3 rounded-lg px-2 text-left hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-fuchsia-600 aria-pressed:bg-fuchsia-50">
          <span class="tsb-ws-opt-logo grid size-8 shrink-0 place-items-center rounded-lg bg-emerald-600 text-xs font-bold text-white" aria-hidden="true">MR</span>
          <span class="min-w-0 flex-1"><span class="block truncate text-sm font-semibold text-zinc-900">Mossbank Records</span><span class="block truncate text-xs text-zinc-600">Free plan, 3 members</span></span>
          <svg class="invisible size-5 shrink-0 text-fuchsia-700 group-aria-pressed:visible" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"/></svg>
        </button></li>
        <li><button type="button" aria-pressed="false" data-name="Cobalt Labs" data-plan="Business plan, 48 members" data-color="bg-blue-600" class="tsb-ws-opt group flex min-h-12 w-full items-center gap-3 rounded-lg px-2 text-left hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-fuchsia-600 aria-pressed:bg-fuchsia-50">
          <span class="tsb-ws-opt-logo grid size-8 shrink-0 place-items-center rounded-lg bg-blue-600 text-xs font-bold text-white" aria-hidden="true">CL</span>
          <span class="min-w-0 flex-1"><span class="block truncate text-sm font-semibold text-zinc-900">Cobalt Labs</span><span class="block truncate text-xs text-zinc-600">Business plan, 48 members</span></span>
          <svg class="invisible size-5 shrink-0 text-fuchsia-700 group-aria-pressed:visible" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"/></svg>
        </button></li>
      </ul>
      <div class="my-1.5 h-px bg-zinc-200"></div>
      <button type="button" class="flex min-h-10 w-full items-center gap-3 rounded-lg px-2 text-sm font-medium text-zinc-700 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-fuchsia-600"><span class="grid size-8 place-items-center rounded-lg border border-dashed border-zinc-300"><svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></span>Create workspace</button>
    </div>
    <nav aria-label="Main">
      <ul class="space-y-0.5">
        <li><a href="#" aria-current="page" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-600 aria-[current=page]:bg-zinc-900 aria-[current=page]:text-white"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 11l9-7 9 7M5 9.5V20h5v-6h4v6h5V9.5"/></svg>Home</a></li>
        <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-600 aria-[current=page]:bg-zinc-900 aria-[current=page]:text-white"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>Projects</a></li>
        <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-600 aria-[current=page]:bg-zinc-900 aria-[current=page]:text-white"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 3h7l5 5v13H7zM14 3v5h5"/></svg>Docs</a></li>
        <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-600 aria-[current=page]:bg-zinc-900 aria-[current=page]:text-white"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 19c0-2.8-2.2-5-5-5s-5 2.2-5 5M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7zM16 11a3 3 0 0 0 0-6M20 19c0-2.4-1.4-4.3-3.4-4.8"/></svg>Members</a></li>
        <li><a href="#" class="group flex min-h-10 items-center gap-3 rounded-lg px-3 text-sm font-medium text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-600 aria-[current=page]:bg-zinc-900 aria-[current=page]:text-white"><svg class="size-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2z"/></svg>Settings</a></li>
      </ul>
    </nav>
  </aside>
  <div class="hidden min-w-0 flex-1 p-8 md:block">
    <h2 id="tsb-ws-heading" class="text-lg font-semibold">Atlas Studio</h2>
    <p class="mt-1 text-sm text-zinc-600">Recent activity across your projects.</p>
    <ul class="mt-6 space-y-3 text-sm">
      <li class="rounded-xl bg-white p-4 ring-1 ring-zinc-200">Spring campaign moved to Review</li>
      <li class="rounded-xl bg-white p-4 ring-1 ring-zinc-200">Two new comments on Homepage hero</li>
      <li class="rounded-xl bg-white p-4 ring-1 ring-zinc-200">Invoice 2026-118 paid</li>
    </ul>
  </div>
</div>

Chat channel list with unread and mentions

  • data-unread:
  • group-open/sec:rotate-90
  • bg-[#13262b]
  • bg-white/5
  • ring-2

Open a channel, or collapse a section.

A custom palette from arbitrary values (bg-[#13262b], text-[#a3c0c1], a #2e6f68 active row) keeps every text pair above 4.5:1. Unread channels carry a bare data-unread attribute that the data-unread: variant turns bold and white, mention counts are badges with sr-only text, and presence dots get ring-2 in the panel color so they cut into the avatar. Sections are native details with named groups for the chevrons; the script moves aria-current, clears unread state and updates the channel header.

View code
<div class="flex min-h-[560px] w-full bg-white text-slate-900">
  <aside aria-label="Sidebar" class="flex w-full flex-col bg-[#13262b] text-[#a3c0c1] md:w-64 md:shrink-0">
    <div class="flex h-14 items-center justify-between gap-2 border-b border-white/10 px-4">
      <span class="truncate text-base font-semibold text-white">Kelpie Design</span>
      <button type="button" class="-mr-1.5 grid size-10 place-items-center rounded-lg bg-white/10 text-white hover:bg-white/20 focus-visible:outline-2 focus-visible:outline-[#7fd1c7]"><span class="sr-only">New message</span><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16zM13.5 6.5l4 4"/></svg></button>
    </div>
    <nav aria-label="Channels and direct messages" class="flex-1 space-y-3 overflow-y-auto px-2 py-3">
      <ul>
        <li><a href="#" data-name="Threads" class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white"><svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5h16v11H9l-5 4z"/></svg>Threads</a></li>
        <li><a href="#" data-name="Mentions" data-unread class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white"><svg class="size-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0zM16 12v1.5a2.5 2.5 0 0 0 5 0V12a9 9 0 1 0-3.5 7.1"/></svg>Mentions<span class="tsb-chat-badge ml-auto rounded-full bg-[#c2412d] px-1.5 text-xs font-bold text-white">2<span class="sr-only"> new</span></span></a></li>
      </ul>
      <details open class="group/sec">
        <summary class="flex min-h-9 cursor-pointer list-none items-center gap-1.5 rounded-md px-2 text-sm font-semibold text-[#e3efef] hover:bg-white/5 focus-visible:outline-2 focus-visible:outline-[#7fd1c7] [&::-webkit-details-marker]:hidden"><svg class="size-4 group-open/sec:rotate-90 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>Channels</summary>
        <ul class="mt-0.5">
          <li><a href="#" aria-current="page" data-name="# general" class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white"><span aria-hidden="true" class="text-[#7fa3a5] group-aria-[current=page]:text-white">#</span>general</a></li>
          <li><a href="#" data-unread data-name="# design-crit" class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white"><span aria-hidden="true" class="text-[#7fa3a5] group-aria-[current=page]:text-white">#</span>design-crit</a></li>
          <li><a href="#" data-unread data-name="# releases" class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white"><span aria-hidden="true" class="text-[#7fa3a5] group-aria-[current=page]:text-white">#</span>releases<span class="tsb-chat-badge ml-auto rounded-full bg-[#c2412d] px-1.5 text-xs font-bold text-white">3<span class="sr-only"> mentions</span></span></a></li>
          <li><a href="#" data-name="# client-harbor" class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white"><span aria-hidden="true" class="text-[#7fa3a5] group-aria-[current=page]:text-white">#</span>client-harbor</a></li>
          <li><a href="#" data-name="# random" class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white opacity-70"><span aria-hidden="true" class="text-[#7fa3a5] group-aria-[current=page]:text-white">#</span>random</a></li>
        </ul>
      </details>
      <details open class="group/sec">
        <summary class="flex min-h-9 cursor-pointer list-none items-center gap-1.5 rounded-md px-2 text-sm font-semibold text-[#e3efef] hover:bg-white/5 focus-visible:outline-2 focus-visible:outline-[#7fd1c7] [&::-webkit-details-marker]:hidden"><svg class="size-4 group-open/sec:rotate-90 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>Direct messages</summary>
        <ul class="mt-0.5">
          <li><a href="#" data-name="Ines Moreau" class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white"><span class="relative grid size-6 shrink-0 place-items-center rounded-md bg-amber-300 text-[11px] font-bold text-[#13262b]" aria-hidden="true">IM<span class="absolute -right-1 -bottom-1 size-2.5 rounded-full bg-emerald-400 ring-2 ring-[#13262b]"></span></span>Ines Moreau<span class="sr-only">, online</span></a></li>
          <li><a href="#" data-name="Kofi Asante" class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white"><span class="relative grid size-6 shrink-0 place-items-center rounded-md bg-sky-300 text-[11px] font-bold text-[#13262b]" aria-hidden="true">KA<span class="absolute -right-1 -bottom-1 size-2.5 rounded-full bg-amber-400 ring-2 ring-[#13262b]"></span></span>Kofi Asante<span class="sr-only">, away</span></a></li>
          <li><a href="#" data-name="Lena Fischer" class="tsb-chat-link group flex min-h-9 items-center gap-2 rounded-md px-2 text-[15px] text-[#a3c0c1] hover:bg-white/5 hover:text-white focus-visible:outline-2 focus-visible:outline-[#7fd1c7] data-unread:font-semibold data-unread:text-white aria-[current=page]:bg-[#2e6f68] aria-[current=page]:text-white"><span class="relative grid size-6 shrink-0 place-items-center rounded-md bg-pink-300 text-[11px] font-bold text-[#13262b]" aria-hidden="true">LF<span class="absolute -right-1 -bottom-1 size-2.5 rounded-full bg-[#4b6668] ring-2 ring-[#13262b]"></span></span>Lena Fischer<span class="sr-only">, offline</span></a></li>
        </ul>
      </details>
    </nav>
  </aside>
  <div class="hidden min-w-0 flex-1 flex-col md:flex">
    <div class="flex h-14 items-center border-b border-slate-200 px-6"><h2 id="tsb-chat-title" class="text-base font-semibold"># general</h2></div>
    <ul class="flex-1 space-y-5 p-6 text-sm">
      <li class="flex gap-3"><span class="grid size-9 shrink-0 place-items-center rounded-lg bg-amber-300 text-xs font-bold text-[#13262b]" aria-hidden="true">IM</span><div><p class="font-semibold">Ines Moreau <span class="font-normal text-slate-500">10:02</span></p><p class="text-slate-700">Harbor signed off on the second logo round. Files are in the shared drive.</p></div></li>
      <li class="flex gap-3"><span class="grid size-9 shrink-0 place-items-center rounded-lg bg-sky-300 text-xs font-bold text-[#13262b]" aria-hidden="true">KA</span><div><p class="font-semibold">Kofi Asante <span class="font-normal text-slate-500">10:09</span></p><p class="text-slate-700">Nice. I will start the stationery mockups after lunch.</p></div></li>
    </ul>
  </div>
</div>

Docs, contents and filter sidebars

Sidebars that sit next to long content: nested documentation sections on native details, a sticky table of contents that follows the reader, and a shop filter panel that turns into a disclosure on phones.

Docs sidebar with nested details

  • <details>
  • group-open/sec:
  • group-open/sub:
  • group-has-[[aria-current=page]]/sec:
  • [&::-webkit-details-marker]:hidden

Open and close the sections.

Every section is a native details element, so opening and closing works with no script and is announced as expanded or collapsed. Each level names its own group, group/sec and group/sub, so a chevron only rotates for its own details; an unnamed group-open: would also match any open ancestor. The section that holds the current page starts open and group-has-[[aria-current=page]]/sec: colors its summary, while list-none and [&::-webkit-details-marker]:hidden remove the default triangle.

View code
<nav aria-label="Docs" class="w-full max-w-xs rounded-2xl border border-slate-200 bg-white p-3 text-sm text-slate-900 shadow-sm">
  <ul class="space-y-1">
    <li>
      <details class="group/sec">
        <summary class="flex min-h-10 cursor-pointer list-none items-center justify-between gap-2 rounded-lg px-3 font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-teal-600 [&::-webkit-details-marker]:hidden group-has-[[aria-current=page]]/sec:text-teal-800">Getting started<svg class="size-4 shrink-0 text-slate-400 group-open/sec:rotate-90 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></summary>
        <ul class="mt-1 mb-2 ml-3 border-l border-slate-200">
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Installation</a></li>
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Editor setup</a></li>
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Upgrade guide</a></li>
        </ul>
      </details>
    </li>
    <li>
      <details open class="group/sec">
        <summary class="flex min-h-10 cursor-pointer list-none items-center justify-between gap-2 rounded-lg px-3 font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-teal-600 [&::-webkit-details-marker]:hidden group-has-[[aria-current=page]]/sec:text-teal-800">Core concepts<svg class="size-4 shrink-0 text-slate-400 group-open/sec:rotate-90 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></summary>
        <ul class="mt-1 mb-2 ml-3 border-l border-slate-200">
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Utility classes</a></li>
            <li>
              <details open class="group/sub">
                <summary class="-ml-px flex min-h-9 cursor-pointer list-none items-center justify-between gap-2 border-l border-transparent pr-3 pl-4 text-slate-700 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 [&::-webkit-details-marker]:hidden">States and variants<svg class="size-4 shrink-0 text-slate-400 group-open/sub:rotate-90 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></summary>
                <ul class="ml-4 border-l border-slate-200">
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Hover and focus</a></li>
            <li><a href="#" aria-current="page" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Dark mode</a></li>
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Responsive design</a></li>
                </ul>
              </details>
            </li>
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Theme variables</a></li>
        </ul>
      </details>
    </li>
    <li>
      <details id="tsb-doc-layout" class="group/sec">
        <summary class="flex min-h-10 cursor-pointer list-none items-center justify-between gap-2 rounded-lg px-3 font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-teal-600 [&::-webkit-details-marker]:hidden group-has-[[aria-current=page]]/sec:text-teal-800">Layout<svg class="size-4 shrink-0 text-slate-400 group-open/sec:rotate-90 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></summary>
        <ul class="mt-1 mb-2 ml-3 border-l border-slate-200">
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Flexbox</a></li>
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Grid</a></li>
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Position</a></li>
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Container queries</a></li>
        </ul>
      </details>
    </li>
    <li>
      <details class="group/sec">
        <summary class="flex min-h-10 cursor-pointer list-none items-center justify-between gap-2 rounded-lg px-3 font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-teal-600 [&::-webkit-details-marker]:hidden group-has-[[aria-current=page]]/sec:text-teal-800">Typography<svg class="size-4 shrink-0 text-slate-400 group-open/sec:rotate-90 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></summary>
        <ul class="mt-1 mb-2 ml-3 border-l border-slate-200">
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Font family</a></li>
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Line height</a></li>
            <li><a href="#" class="-ml-px flex min-h-9 items-center border-l border-transparent pl-4 text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-teal-600 aria-[current=page]:border-teal-600 aria-[current=page]:font-semibold aria-[current=page]:text-teal-800">Text wrap</a></li>
        </ul>
      </details>
    </li>
  </ul>
</nav>

Sticky table of contents with scroll spy

  • sticky top-6
  • md:grid-cols-[minmax(0,1fr)_13rem]
  • aria-[current=location]:
  • scroll-mt-*
  • motion-safe:scroll-smooth

Scroll the article, or click an entry in the contents.

The right-hand nav is a grid column that stretches to the full article height, and its inner list is sticky top-6, so it stays in view while the scroll box moves. An IntersectionObserver rooted on that box with a -65% bottom margin marks the first section in the top third as current through aria-current="location", which aria-[current=location]: styles; a scroll check selects the last section at the very end. Phones get a sticky bar with the current section name instead, scroll-mt-* keeps headings clear of it, and motion-safe:scroll-smooth animates link jumps only when motion is welcome.

View code
<div id="tsb-toc-box" class="h-[480px] w-full overflow-y-auto bg-white text-slate-700 motion-safe:scroll-smooth">
  <p class="sticky top-0 z-10 border-b border-slate-200 bg-white px-4 py-3 text-sm md:hidden"><span class="text-slate-600">On this page:</span> <span id="tsb-toc-current" class="font-semibold text-red-800">Overview</span></p>
  <div class="mx-auto grid max-w-4xl gap-10 px-4 pb-10 md:grid-cols-[minmax(0,1fr)_13rem] md:px-8">
    <article class="min-w-0 leading-relaxed">
      <h2 class="pt-6 text-2xl font-semibold tracking-tight text-slate-900 md:pt-10">Shipping design tokens to three platforms</h2>
      <p class="mt-2 text-sm text-slate-600">Platform team handbook, updated October 2026</p>
      <section id="tsb-toc-s0" class="tsb-toc-section scroll-mt-16 pt-6 md:scroll-mt-6">
        <h3 class="text-lg font-semibold text-slate-900">Overview</h3>
        <p class="mt-3">A design token is a named value, such as a color or a spacing step, that every platform reads from one source file. The goal of this guide is a single change in that file reaching the web app, the iOS app and the Android app in the same release.</p>
        <p class="mt-3">The pipeline has three stages: write tokens in JSON, transform them per platform, and publish the results as packages that each app installs.</p>
      </section>
      <section id="tsb-toc-s1" class="tsb-toc-section scroll-mt-16 pt-6 md:scroll-mt-6">
        <h3 class="text-lg font-semibold text-slate-900">Naming tokens</h3>
        <p class="mt-3">Name tokens by role, not by value. A token called color-text-muted survives a palette change, while gray-500 turns into a lie the day the gray shifts.</p>
        <p class="mt-3">Keep two tiers: base tokens that hold raw values, and semantic tokens that point at them. Apps should only ever use the semantic tier.</p>
      </section>
      <section id="tsb-toc-s2" class="tsb-toc-section scroll-mt-16 pt-6 md:scroll-mt-6">
        <h3 class="text-lg font-semibold text-slate-900">Color scales</h3>
        <p class="mt-3">Generate each scale in OKLCH so steps look evenly spaced. Check every text pair against a 4.5:1 contrast ratio before it ships, in both light and dark themes.</p>
        <p class="mt-3">Dark mode gets its own semantic layer that points to different base steps, rather than a second copy of every token.</p>
      </section>
      <section id="tsb-toc-s3" class="tsb-toc-section scroll-mt-16 pt-6 md:scroll-mt-6">
        <h3 class="text-lg font-semibold text-slate-900">Spacing and type</h3>
        <p class="mt-3">A 4px base step covers almost every layout need. Type sizes come with their line heights as pairs, so nobody combines a 14px size with a 32px line height by accident.</p>
      </section>
      <section id="tsb-toc-s4" class="tsb-toc-section scroll-mt-16 pt-6 md:scroll-mt-6">
        <h3 class="text-lg font-semibold text-slate-900">Theming</h3>
        <p class="mt-3">Each brand theme overrides a handful of semantic tokens and nothing else. If a theme needs to touch components, the token set is missing a role.</p>
        <p class="mt-3">On the web the build writes CSS custom properties, so switching themes is a class change on the root element.</p>
      </section>
      <section id="tsb-toc-s5" class="tsb-toc-section scroll-mt-16 pt-6 md:scroll-mt-6">
        <h3 class="text-lg font-semibold text-slate-900">Release checklist</h3>
        <p class="mt-3">Bump the package version, publish the changelog, update the three apps in one pull request each, and post screenshots of the before and after states in the release channel.</p>
      </section>
    </article>
    <nav aria-label="On this page" class="hidden md:block">
      <div class="sticky top-6 pt-10">
        <h3 class="text-sm font-semibold text-slate-900">On this page</h3>
        <ul class="mt-3 border-l border-slate-200">
          <li><a href="#tsb-toc-s0" aria-current="location" class="tsb-toc-link -ml-px block border-l-2 border-transparent py-1.5 pl-4 text-sm text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-red-700 aria-[current=location]:border-red-700 aria-[current=location]:font-medium aria-[current=location]:text-red-800">Overview</a></li>
          <li><a href="#tsb-toc-s1" class="tsb-toc-link -ml-px block border-l-2 border-transparent py-1.5 pl-4 text-sm text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-red-700 aria-[current=location]:border-red-700 aria-[current=location]:font-medium aria-[current=location]:text-red-800">Naming tokens</a></li>
          <li><a href="#tsb-toc-s2" class="tsb-toc-link -ml-px block border-l-2 border-transparent py-1.5 pl-4 text-sm text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-red-700 aria-[current=location]:border-red-700 aria-[current=location]:font-medium aria-[current=location]:text-red-800">Color scales</a></li>
          <li><a href="#tsb-toc-s3" class="tsb-toc-link -ml-px block border-l-2 border-transparent py-1.5 pl-4 text-sm text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-red-700 aria-[current=location]:border-red-700 aria-[current=location]:font-medium aria-[current=location]:text-red-800">Spacing and type</a></li>
          <li><a href="#tsb-toc-s4" class="tsb-toc-link -ml-px block border-l-2 border-transparent py-1.5 pl-4 text-sm text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-red-700 aria-[current=location]:border-red-700 aria-[current=location]:font-medium aria-[current=location]:text-red-800">Theming</a></li>
          <li><a href="#tsb-toc-s5" class="tsb-toc-link -ml-px block border-l-2 border-transparent py-1.5 pl-4 text-sm text-slate-600 hover:border-slate-400 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-red-700 aria-[current=location]:border-red-700 aria-[current=location]:font-medium aria-[current=location]:text-red-800">Release checklist</a></li>
        </ul>
      </div>
    </nav>
  </div>
</div>

Shop filter sidebar

  • appearance-none checked:
  • peer-checked:
  • accent-stone-900
  • md:hidden summary
  • border-[#b98a56]

Change the filters. On a phone, open Filters first.

The filters are plain form controls styled with utilities: checkboxes with appearance-none and checked: colors plus a peer-checked:visible check icon, frame swatches and size pills as sr-only radios with peer-checked: rings, and a range input tinted with accent-stone-900. On phones the panel is a details element with a Filters summary, and from md: the summary hides and the script keeps it open. The script filters the grid, recalculates prices for the size, swaps the frame border class (all three class names are written in full) and announces the count.

View code
<div class="w-full bg-white text-stone-900 md:flex">
  <details id="tsb-shop-filters" open class="group/f border-b border-stone-200 md:w-64 md:shrink-0 md:border-r md:border-b-0">
    <summary class="flex min-h-12 cursor-pointer list-none items-center justify-between px-4 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-stone-900 md:hidden [&::-webkit-details-marker]:hidden">Filters<svg class="size-5 text-stone-500 group-open/f:rotate-180 motion-safe:transition-transform" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg></summary>
    <form id="tsb-shop-form" aria-label="Filter prints" class="space-y-7 px-4 pt-2 pb-6 md:p-6">
      <fieldset>
        <legend class="text-sm font-semibold">Subject</legend>
        <ul class="mt-3 space-y-3">
        <li class="relative flex items-center gap-3">
          <input id="tsb-shop-water" type="checkbox" name="tsb-cat" value="water" class="peer size-5 shrink-0 cursor-pointer appearance-none rounded border border-stone-400 bg-white checked:border-stone-900 checked:bg-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">
          <svg class="pointer-events-none invisible absolute top-1/2 left-0.5 size-4 -translate-y-1/2 text-white peer-checked:visible" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"/></svg>
          <label for="tsb-shop-water" class="flex flex-1 cursor-pointer justify-between text-sm text-stone-700">Water<span class="text-stone-500">2</span></label>
        </li>
        <li class="relative flex items-center gap-3">
          <input id="tsb-shop-mountains" type="checkbox" name="tsb-cat" value="mountains" class="peer size-5 shrink-0 cursor-pointer appearance-none rounded border border-stone-400 bg-white checked:border-stone-900 checked:bg-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">
          <svg class="pointer-events-none invisible absolute top-1/2 left-0.5 size-4 -translate-y-1/2 text-white peer-checked:visible" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"/></svg>
          <label for="tsb-shop-mountains" class="flex flex-1 cursor-pointer justify-between text-sm text-stone-700">Mountains<span class="text-stone-500">2</span></label>
        </li>
        <li class="relative flex items-center gap-3">
          <input id="tsb-shop-cities" type="checkbox" name="tsb-cat" value="cities" class="peer size-5 shrink-0 cursor-pointer appearance-none rounded border border-stone-400 bg-white checked:border-stone-900 checked:bg-stone-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">
          <svg class="pointer-events-none invisible absolute top-1/2 left-0.5 size-4 -translate-y-1/2 text-white peer-checked:visible" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"/></svg>
          <label for="tsb-shop-cities" class="flex flex-1 cursor-pointer justify-between text-sm text-stone-700">Cities<span class="text-stone-500">2</span></label>
        </li>
        </ul>
      </fieldset>
      <fieldset>
        <legend class="text-sm font-semibold">Frame <span id="tsb-shop-frame-name" class="font-normal text-stone-600">Black</span></legend>
        <div class="mt-3 flex gap-3">
        <label class="cursor-pointer">
          <input type="radio" name="tsb-frame" value="black" class="peer sr-only" checked>
          <span class="block size-9 rounded-full bg-stone-900 ring-1 ring-stone-300 ring-offset-2 peer-checked:ring-2 peer-checked:ring-stone-900 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-4 peer-focus-visible:outline-stone-900"></span>
          <span class="sr-only">Black</span>
        </label>
        <label class="cursor-pointer">
          <input type="radio" name="tsb-frame" value="oak" class="peer sr-only">
          <span class="block size-9 rounded-full bg-[#b98a56] ring-1 ring-stone-300 ring-offset-2 peer-checked:ring-2 peer-checked:ring-stone-900 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-4 peer-focus-visible:outline-stone-900"></span>
          <span class="sr-only">Natural oak</span>
        </label>
        <label class="cursor-pointer">
          <input type="radio" name="tsb-frame" value="white" class="peer sr-only">
          <span class="block size-9 rounded-full bg-white ring-1 ring-stone-300 ring-offset-2 peer-checked:ring-2 peer-checked:ring-stone-900 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-4 peer-focus-visible:outline-stone-900"></span>
          <span class="sr-only">White</span>
        </label>
        </div>
      </fieldset>
      <fieldset>
        <legend class="text-sm font-semibold">Size</legend>
        <div class="mt-3 grid grid-cols-3 gap-2">
        <label class="block">
          <input type="radio" name="tsb-size" value="s" class="peer sr-only" checked>
          <span class="flex min-h-10 cursor-pointer items-center justify-center rounded-lg border border-stone-300 px-2 text-sm font-medium text-stone-700 peer-checked:border-stone-900 peer-checked:bg-stone-900 peer-checked:text-white peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-stone-900 hover:border-stone-500">30x40</span>
        </label>
        <label class="block">
          <input type="radio" name="tsb-size" value="m" class="peer sr-only">
          <span class="flex min-h-10 cursor-pointer items-center justify-center rounded-lg border border-stone-300 px-2 text-sm font-medium text-stone-700 peer-checked:border-stone-900 peer-checked:bg-stone-900 peer-checked:text-white peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-stone-900 hover:border-stone-500">50x70</span>
        </label>
        <label class="block">
          <input type="radio" name="tsb-size" value="l" class="peer sr-only">
          <span class="flex min-h-10 cursor-pointer items-center justify-center rounded-lg border border-stone-300 px-2 text-sm font-medium text-stone-700 peer-checked:border-stone-900 peer-checked:bg-stone-900 peer-checked:text-white peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-stone-900 hover:border-stone-500">70x100</span>
        </label>
        </div>
      </fieldset>
      <div>
        <div class="flex items-center justify-between"><label for="tsb-shop-max" class="text-sm font-semibold">Max price</label><output id="tsb-shop-max-out" for="tsb-shop-max" class="text-sm text-stone-600">$120</output></div>
        <input id="tsb-shop-max" type="range" min="20" max="120" step="5" value="120" class="mt-3 h-2 w-full cursor-pointer accent-stone-900">
      </div>
      <button type="reset" class="flex min-h-11 w-full items-center justify-center rounded-lg border border-stone-300 text-sm font-semibold text-stone-800 hover:bg-stone-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-stone-900">Reset filters</button>
    </form>
  </details>
  <div class="min-w-0 flex-1 p-4 md:p-6">
    <div class="flex items-baseline justify-between gap-3">
      <h2 class="text-lg font-semibold">Landscape prints</h2>
      <p id="tsb-shop-count" aria-live="polite" class="text-sm text-stone-600">6 prints</p>
    </div>
    <ul class="mt-4 grid grid-cols-2 gap-x-3 gap-y-5 md:grid-cols-3 md:gap-x-5">
      <li class="tsb-shop-item" data-cat="water" data-base="32">
        <div class="tsb-shop-frame border-[6px] border-stone-900 bg-white shadow-sm ring-1 ring-stone-200 md:border-8"><img src="fjord.webp" width="800" height="600" alt="Deep blue fjord between grey cliffs under a cloudy sky" loading="lazy" class="aspect-[4/3] w-full object-cover"></div>
        <p class="mt-2 truncate text-sm font-medium text-stone-900">Fjord from the cliff edge</p>
        <p class="tsb-shop-price text-sm text-stone-600">$32</p>
      </li>
      <li class="tsb-shop-item" data-cat="water" data-base="28">
        <div class="tsb-shop-frame border-[6px] border-stone-900 bg-white shadow-sm ring-1 ring-stone-200 md:border-8"><img src="forest-waterfall.webp" width="800" height="600" alt="Waterfall dropping into a mossy green gorge in a pine forest" loading="lazy" class="aspect-[4/3] w-full object-cover"></div>
        <p class="mt-2 truncate text-sm font-medium text-stone-900">Gorge waterfall</p>
        <p class="tsb-shop-price text-sm text-stone-600">$28</p>
      </li>
      <li class="tsb-shop-item" data-cat="mountains" data-base="36">
        <div class="tsb-shop-frame border-[6px] border-stone-900 bg-white shadow-sm ring-1 ring-stone-200 md:border-8"><img src="granite-valley.webp" width="800" height="600" alt="Granite peaks above pine trees and a calm river" loading="lazy" class="aspect-[4/3] w-full object-cover"></div>
        <p class="mt-2 truncate text-sm font-medium text-stone-900">Granite valley</p>
        <p class="tsb-shop-price text-sm text-stone-600">$36</p>
      </li>
      <li class="tsb-shop-item" data-cat="mountains" data-base="30">
        <div class="tsb-shop-frame border-[6px] border-stone-900 bg-white shadow-sm ring-1 ring-stone-200 md:border-8"><img src="red-rock-canyon.webp" width="800" height="600" alt="Red rock ridge glowing in late evening light" loading="lazy" class="aspect-[4/3] w-full object-cover"></div>
        <p class="mt-2 truncate text-sm font-medium text-stone-900">Red rock at dusk</p>
        <p class="tsb-shop-price text-sm text-stone-600">$30</p>
      </li>
      <li class="tsb-shop-item" data-cat="cities" data-base="42">
        <div class="tsb-shop-frame border-[6px] border-stone-900 bg-white shadow-sm ring-1 ring-stone-200 md:border-8"><img src="city-skyline.webp" width="800" height="600" alt="City skyline at sunrise with a lake on the horizon" loading="lazy" class="aspect-[4/3] w-full object-cover"></div>
        <p class="mt-2 truncate text-sm font-medium text-stone-900">Skyline at sunrise</p>
        <p class="tsb-shop-price text-sm text-stone-600">$42</p>
      </li>
      <li class="tsb-shop-item" data-cat="cities" data-base="26">
        <div class="tsb-shop-frame border-[6px] border-stone-900 bg-white shadow-sm ring-1 ring-stone-200 md:border-8"><img src="canal-houses.webp" width="800" height="600" alt="Colorful canal houses with small boats moored along the water" loading="lazy" class="aspect-[4/3] w-full object-cover"></div>
        <p class="mt-2 truncate text-sm font-medium text-stone-900">Canal houses</p>
        <p class="tsb-shop-price text-sm text-stone-600">$26</p>
      </li>
    </ul>
    <p id="tsb-shop-empty" hidden class="mt-4 rounded-xl border border-dashed border-stone-300 px-4 py-10 text-center text-sm text-stone-600">No prints match. Try a higher price or fewer subjects.</p>
  </div>
</div>

Which layout method works best for a Tailwind sidebar?

Dashboards with long pages usually want fixed positioning. If a header or footer shares the screen, a sticky sidebar inside a flex or grid wrapper is the better fit.

On desktop the two look identical. What separates them is whether the sidebar leaves the document flow, and that decides how much offset math the main column needs.

Fixed positioning

A fixed sidebar stays put while the page scrolls. Tailwind’s position documentation says fixed offsets are calculated relative to the viewport, not the parent.

  • fixed inset-y-0 left-0 w-64 locks the panel to the left edge at full height
  • Since the panel leaves the document flow, the main content needs a matching left offset such as lg:pl-64
  • Add overflow-y-auto and long menus scroll inside the panel

The annoying part is that the width lives in two places. Change w-64 on the sidebar and the padding on the content has to change with it, which is easy to forget.

Sticky inside a flex or grid wrapper

Sticky behaves as relative until the page scrolls past an offset, then holds position until its parent leaves the screen (Tailwind docs, position utilities). The parent has to be as tall as the page (an easy detail to miss), or the sidebar stops sticking early.

  • A flex shell puts an aside with w-64 shrink-0 next to a main with flex-1. The shrink-0 stops the sidebar from compressing.
  • In a grid shell, give the first column a fixed width, let the second one flex, and set the aside to sticky top-0 h-screen.
  • Put overflow-y-auto on the aside so long menus stay reachable.

When a top header spans both columns, sticky saves the offset math. For a dashboard built with Tailwind that has such a header, it’s the better default.

Sidebar width values

Tailwind’s width documentation builds w-64 from the spacing scale, which comes to 16rem (256px) by default and is the usual starting point.

Labels running long? Go to w-72 (18rem) or w-80 (20rem). An icon rail wants w-14 or w-16.

Size it to the longest label, not to the mockup. Anything off the scale takes an arbitrary value such as w-[17rem].

How to make a Tailwind sidebar responsive with an off-canvas drawer

Hide the sidebar below a breakpoint with -translate-x-full and restore it with lg:translate-x-0, add a transition for the slide, and place a fixed backdrop behind the panel. A toggle button then flips one class or one state value.

Tailwind’s responsive utilities follow a mobile-first design approach. Unprefixed utilities apply everywhere, and breakpoint prefixes override them upward. The lg: prefix starts at 64rem (1024px) in the default scale (Tailwind CSS v4.3 docs).

Below that width the sidebar behaves as a UI drawer, a panel that slides over the content and closes on an outside tap.

The steps go in this order.

  1. Give the panel fixed inset-y-0 left-0 z-50 w-64 -translate-x-full transition-transform duration-200 lg:translate-x-0.
  2. Add the backdrop with fixed inset-0 z-40 bg-black/50 lg:hidden, rendered only while the drawer is open.
  3. On open, swap -translate-x-full for translate-x-0, through Alpine.js :class or a React conditional.
  4. Put a menu button in the top bar with lg:hidden so it disappears on desktop.
  5. Close the drawer on backdrop click, link click and the Escape key.
  6. Add motion-reduce:transition-none for visitors who asked for less movement.

The motion-reduce variant maps to the prefers-reduced-motion: reduce query (Tailwind CSS v4.3 docs), so step 6 needs no JavaScript.

Pick md for narrow sidebars and lg for wide ones. A 256px sidebar on a 768px (md) screen leaves 512px for content, which is tight for tables.

The backdrop sits one level below the panel, and both sit above any sticky header.

Toggle and collapse state in a Tailwind sidebar

Open and closed state can come from a hidden checkbox, an Alpine.js x-data scope, React state or a data attribute. They all flip the same classes. What differs is how each one fails.

Choosing a state driver

DriverNeeds JavaScriptWhere state livesFails when
Checkbox with peerNoThe checkbox itselfThe input comes after the sidebar in the markup
Alpine.js x-dataYes (Alpine)An x-data scope on a shared parentThe toggle button sits outside that scope
React useStateYes (React)The layout componentThe hook runs in a Next.js Server Component
Data attributeYes (something sets it)A data-state attribute on the panelNothing updates the attribute

daisyUI takes the checkbox route. Its drawer uses a hidden drawer-toggle input, and lg:drawer-open keeps the panel visible from the lg breakpoint up (daisyUI docs).

For Alpine, bind the translate class with :class rather than relying on x-transition. The Alpine.js documentation says x-transition by itself applies a fade and scale, not a slide.

React projects hold the open flag in the layout component, which assumes you already have Tailwind running inside a React project. In the Next.js App Router, that component needs the “use client” directive because hooks do not run in Server Components (Next.js docs).

On small screens the trigger is usually a hamburger menu button in the top bar.

Collapsing to an icon rail

daisyUI’s icon-only drawer example swaps the panel between is-drawer-close:w-14 and is-drawer-open:w-64 (daisyUI docs).

Labels disappear when the panel is closed, and hover tooltips keep the icons identifiable.

shadcn/ui reaches the same result with a prop. Its Sidebar component accepts collapsible set to offcanvas, icon or none, and the useSidebar hook returns toggleSidebar, per the shadcn/ui documentation.

The component source also defines a keyboard shortcut constant set to “b”. Check it against the shortcuts your app already uses.

How to style active, hover and focus states for sidebar links

The current page gets styled from aria-current="page" through an arbitrary aria variant. Pointer feedback comes from hover:, and the keyboard ring from focus-visible:. Tying the active look to the attribute means one piece of markup both tells assistive technology where the user is and drives the color.

In light mode, hover is hover:bg-gray-100, and dark:hover:bg-gray-800 covers dark. For focus, focus-visible:outline-2 focus-visible:outline-offset-2 applies only when the link is focused by keyboard. The current page takes aria-[current=page]:bg-gray-200 aria-[current=page]:font-medium.

The bracket syntax is needed because Tailwind’s built-in aria variants only match common boolean attributes set to true, such as aria-expanded and aria-selected (v4.3 docs, October 2026). The aria-current attribute takes values like page and step, so it falls outside that list.

These variants mirror the pseudo-classes from classic CSS link hover effects, only written as utilities and stackable with dark: and breakpoint prefixes.

Heroicons, made by Tailwind Labs, ships 24px outline, 24px solid, 20px solid and 16px solid sets (Heroicons repository). Lucide’s React package draws strokes in currentColor by default, so both families pick up the link’s text color.

Copy the icon source and inline the SVG in HTML, then size it on the element with size-5 shrink-0. The Heroicons README shows the same pattern with size-6 text-gray-500.

In the icon-only rail, hide the text with sr-only instead of removing it, so the link keeps its accessible name. Swap to the solid icon variant on the active row when the icon should carry the state too.

Dark mode setup for a Tailwind sidebar

Out of the box, dark: follows the operating system. A manual toggle takes one line of CSS that overrides the variant.

  • With the system setting you do nothing, since prefers-color-scheme drives it.
  • For a class toggle, add @custom-variant dark (&:where(.dark, .dark *)); to your CSS and put a dark class on the html element.
  • For a data attribute, use @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); with data-theme="dark" on html.

The default variant compiles to a prefers-color-scheme media query, which is why a toggled class does nothing until the override is in place. Skip it and the utilities keep following the OS.

Older tutorials set darkMode: 'class' in tailwind.config.js. Tailwind v3.4.1 added 'selector' as the newer strategy and kept 'class' working as before, and the v4.3 documentation handles the choice in CSS with @custom-variant.

Tailwind’s dark mode page also shows a three-way toggle that reads localStorage.theme first and falls back to window.matchMedia when nothing is stored. It recommends running that script inline in the head to avoid a flash of the wrong theme.

Sidebar partLightDark
Panelbg-whitedark:bg-gray-900
Borderborder-gray-200dark:border-gray-800
Link texttext-gray-700dark:text-gray-300
Active rowbg-gray-200dark:bg-gray-700

Check the color contrast of each pair in both modes, and of the focus ring against the active row. A ring picked for the light panel can vanish against dark gray, so test it in both.

How to build nested submenus in a Tailwind sidebar

Wrap each parent link in a details element, put the child links in a list inside it, and let the browser handle the toggle. When the menu needs JavaScript state anyway, a button with aria-expanded and a hidden list does the same job through Alpine.js or React.

The native route is the same disclosure pattern behind a Tailwind accordion, with links where the answers would be.

  • The summary element is the parent row, and siblings that share a name attribute form a group where at most one can be open (HTML Living Standard).
  • Tailwind’s open variant styles the details element itself, for example open:bg-gray-100.
  • For the chevron, name a group with group/sub on the details element and put group-open/sub:rotate-180 on the icon.

If you’d rather keep state in code, Alpine.js needs an x-data flag on the list item and x-show on the nested list. React takes one useState per group, or one object keyed by group id.

Either way, expand the group that holds the current page on first render. A submenu that hides the active link forces a click just to see where you are.

The group name is not decoration. An unnamed group-open: matches any open ancestor marked with group, so opening a parent rotates the chevrons of its collapsed children until each rotation is scoped to its own group.

Stop at two levels. A third level runs out of width in a narrow panel, and in the icon rail nested lists have no room at all, so the parent becomes a flyout built like a Tailwind dropdown anchored to the rail.

Libraries handle this with their own plumbing. Preline UI lists collapsible nav groups among its sidebar examples, next to header and footer dropdowns (Preline docs). Flowbite’s multi-level example toggles a hidden list through a data-collapse-toggle attribute (Flowbite v4.0 docs). David UI from Creative Tim asks for initCollapse() or its CDN collapse script before the nested menus respond.

What makes a Tailwind sidebar accessible?

The markup has to carry it, because Tailwind classes style these states but never add them. That means a labeled nav element, aria-expanded and aria-controls on every toggle, aria-current="page" on the current link, and a drawer that traps focus and closes on Escape.

MDN prefers the nav element for the landmark, since it exposes the navigation role on its own, and asks for a label on each one when a page holds more than one. Use Main and Footer rather than Main navigation, since screen readers already announce the role.

The toggle should be a real button with aria-expanded and aria-controls pointing at the panel, the pairing used in the W3C APG disclosure navigation example.

Put aria-current="page" on one link at a time. MDN lists seven accepted values (page, step, location, date, time, true and false), and page is the one for site navigation.

Tab order follows the DOM. In the APG example, Escape closes an open dropdown and returns focus to its button.

Do not reach for role="menu" because the sidebar looks like an application menu. The APG disclosure example avoids it on purpose, since site navigation does not need the keyboard behavior that role promises to assistive technology.

The toggle attributes come from the ARIA specification, and the APG example is the version to copy from.

A drawer built from translate classes alone does none of the focus work. Focus has to move into the panel and stay trapped there, and the page behind should go inert. Headless UI’s Dialog documentation covers all of that, plus locked page scroll and close events on Escape or an outside click.

On close, return focus to the menu button that opened the drawer.

Library docs and the standards disagree in places. Flowbite’s default sidebar example (v4.0 docs, October 2026) puts its list inside an aside labeled Sidebar with no nav element, and the toggle button in that markup sets aria-controls without aria-expanded.

Copy the layout from a library if you like, then correct those two attributes against the MDN and APG guidance. Run the result through a web accessibility checklist, then test with the keyboard alone and one screen reader.

Hand-built, Tailwind Plus, Flowbite, daisyUI, Preline UI or shadcn/ui: which route fits?

If the menu has one level of links and one breakpoint, hand-build it. Server-rendered projects suit daisyUI, React apps suit shadcn/ui, and Flowbite or Preline UI make sense when ready-made variants matter more than control.

Tailwind Plus is the odd one out. As of October 2026 its pages redirect to a sign-in form, and Tailwind Labs’ September 9, 2026 announcement closed sign-ups to new customers while existing licensees keep access.

Comparison of the six routes

RouteSetupFramework fitCost or status
Hand-builtUtility classes plus your own state driverAny stackFree
Tailwind PlusCopy-paste sidebar layouts from Application ShellsHTML, React, VueClosed to new customers
Flowbitenpm package plus Flowbite JSIntegration guides in its docsFree MIT core, paid Pro tier
daisyUInpm package plus @plugin “daisyui” in app.cssAny stack, class-basedFree, MIT on npm
Preline UInpm package plus JavaScript pluginsFramework guides in its docsFree core, paid Pro tier
shadcn/uiCLI copies the source into your projectReact first, community ports for Vue and SvelteFree, MIT

Preline’s pricing page sells Pro as a one-time payment for a single developer.

Pros and cons by route

  • Hand-building gives you control over the markup, the state and the semantics. The cost is that every behavior in this guide, from focus trapping to dark mode, is yours to build and test.
  • Tailwind Plus offers layouts from the Tailwind team, and since July 25, 2025 every UI block, plain HTML included, is interactive (Tailwind blog). No new licenses are sold, though, and the announcement does not say whether updates continue.
  • Flowbite documents seven sidebar variants and has integration guides for 20 frameworks, from React to Hugo (Flowbite docs). The drawer and collapse behavior needs Flowbite’s JavaScript file.
  • daisyUI installs as a Tailwind plugin, and its docs carry install guides from Vite to Laravel and Phoenix. The catch is a second vocabulary, component classes and themes, sitting next to your utilities.
  • Preline UI’s sidebar variants include offcanvas, content push and an icon-only mini version (Preline docs). It requires Preline’s JavaScript, and the MIT license comes paired with a Fair Use License, so read both before redistributing code.
  • With shadcn/ui you own the source, and the Sidebar takes sidebar, floating and inset variants. You do have to choose a primitive layer first, since its sidebar docs now show Base UI, React Aria and Radix UI versions.

Decision rule by project type

If the app already has a design system, shadcn/ui’s --sidebar-* CSS variables for background, accent, border and ring drop into it.

  • Static HTML or server-rendered projects (Laravel, Rails, Django) do well with daisyUI, or with Flowbite when its ready-made variants save more time than its script costs.
  • React or Next.js apps should go with shadcn/ui, so the sidebar’s state lives in its provider rather than yours.
  • Vue or Svelte apps can take a community shadcn port, or a hand-built sidebar on the framework’s own state.
  • One level of links and one breakpoint? Build it by hand, because a library adds a dependency to solve a problem you do not have.
  • Anyone with an existing Tailwind Plus license already has its sidebar layouts, which no new customer can buy.

When a hand-built Tailwind sidebar breaks

Hand-built sidebars tend to break in the same spots: viewport height on phones, clipped flyouts, stacking order, library scripts that never ran, and class names Tailwind never generated. Each has a documented cause and a short fix.

SymptomCauseFix
Bottom links hidden on phonesh-screen is a fixed 100vh while browser toolbars expand and contracth-dvh
Flyouts and tooltips cut offoverflow-y-auto makes the horizontal axis clip tooRender flyouts outside the scroll area
Drawer hidden behind the headerAn ancestor creates a stacking context that traps the panel’s z-indexMake the drawer a direct child of the layout root
Library sidebar does nothingScript not loaded, or target id mismatchLoad the script, check the id in the console
Classes missing in productionClass names built from strings, or sources in ignored foldersWrite full class names, add @source

Tailwind’s height docs describe h-dvh as spanning the viewport height as it changes with the browser UI. Dynamic viewport utilities arrived in Tailwind CSS v3.4 on December 19, 2023 (Tailwind blog), so swap h-screen for h-dvh, including in the sticky layout shown earlier.

The clipping one trips people up. MDN documents that a visible overflow value behaves as auto when the other axis is neither visible nor clip. That is why a sidebar with overflow-y-auto also clips sideways.

For stacking, MDN lists fixed or sticky positioning and any translate, transform or filter value other than none as stacking context triggers, and a child’s z-index only counts inside its parent’s context.

On the library side, in Flowbite 2.4’s source, a data-drawer-target that points to a missing id logs a console error naming that id. Open the console first when a pasted sidebar renders but never opens.

Class detection works on plain text. Tailwind scans files as plain text, so text-{{ error ? 'red' : 'green' }}-600 never produces a class. Props should map to complete class names instead (v4.3 docs).

Files in .gitignore and node_modules are skipped during that scan, which is why Preline’s setup registers its scripts with @source.

One disagreement is worth settling. Flowbite’s examples show a permanent sidebar from sm (640px), which leaves 384px beside a w-64 panel, while the md and lg breakpoints recommended above leave more.

Pick the breakpoint from your widest table, because every responsive design pays for the sidebar on every screen where it stays open.

Stop hand-building when the menu reaches three levels or several teams edit it. At that point the focus handling and ARIA states need a tested primitive, not another round of patches.

Tailwind Sidebar FAQ

Does Tailwind CSS include a ready-made sidebar component?

No. Tailwind CSS ships utilities and variants, so a sidebar is an aside styled with classes like fixed, w-64 and translate, or a component taken from a library.

The v4.3 docs list utility categories such as layout, sizing and transforms, with no component section.

Is there a free Tailwind sidebar template?

Yes. Preline UI’s free tier includes 640+ components and five templates, and Flowbite’s sidebar examples are MIT-licensed code you can copy (Preline pricing page, Flowbite docs).

How do I keep the sidebar open or closed after a page reload?

Store the flag outside component state. localStorage suits client-only apps, while a cookie is readable on the server, so the first render already matches.

shadcn/ui takes the cookie route and names it sidebar_state in its docs.

How do I highlight the active sidebar link in Next.js?

Read the path with the usePathname hook from next/navigation, compare it to each link’s href, and set aria-current="page" on the match.

The hook runs only in a Client Component. The Next.js docs say it may return null when a component is imported into a Pages Router route whose router has not initialized yet, such as a fallback route or an automatically statically optimized page.

Can the sidebar sit on the right side of the page?

Yes. Mirror the classes: right-0 instead of left-0, translate-x-full for the closed state, and right padding on the content.

Flowbite’s drawer source uses that pair for its right placement and defaults to left.

How can users resize the sidebar?

Preline UI pairs its sidebar with a layout splitter plugin for drag resizing (Preline docs).

Without a library, keep the width in a CSS variable, update it from a drag handle, and apply it to the panel with w-(--sidebar-width).

Which is better for my app, a sidebar or a top navbar?

A sidebar fits apps with many top-level destinations, such as dashboards, because Nielsen Norman Group notes that vertical left-side navigation can hold as many top-tier items as needed on desktop.

A top navbar fits a handful of links. The two can also be combined, since Flowbite’s docs pair a sidebar with a fixed navbar.

Building a Tailwind Sidebar in the Right Order

A Tailwind sidebar holds up when it is built in a fixed order, with semantics before styling and state before dark mode. Start with semantic markup and aria-current, move on to layout and drawer behavior, and leave state, persistence and dark mode for last.

That sequence keeps each layer testable. A keyboard pass on bare markup catches problems before styling hides them, and dark mode only swaps color pairs, so it never changes structure.

Hand-building trades speed for control. The team owns every focus, scroll-lock and stacking decision, while a library sidebar arrives with those decisions made and with its own defaults to audit.

This was last verified on October 29, 2026. The advice shifts if Tailwind Plus reopens to new customers or Tailwind CSS ships an official sidebar component.