Tailwind + JSMIT licensed

Tailwind CSS Collapsible Sidebar Admin Dashboard

The canonical SaaS shell: top bar, a sidebar that folds from 236px to a 68px icon rail, and a fluid main area. The fold is w-59 → group-data-collapsed/shell:w-17, labels fade with group-data-collapsed/shell:opacity-0, and rail tooltips are generated by after:content-[attr(data-label)]. The script sets one attribute — it never touches a class — and now remembers the choice and answers the [ shortcut.

Live Demo
Plain CSSTailwind v4
Try it

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

Cruip · Mosaic by Cruip · Affiliate link

View Mosaic dashboard

The code

<section class="tdl-02 w-full min-h-svh bg-tdl02-bg font-tdl02-sans text-tdl02-ink antialiased" aria-label="Collapsible sidebar admin shell demo">
  <div data-shell class="group/shell @container/shell flex h-svh min-h-[34rem] flex-col">
    <header class="flex h-15 flex-none items-center gap-3 border-b border-tdl02-edge bg-tdl02-panel px-3 sm:px-4">
      <button data-toggle type="button" aria-expanded="true" aria-controls="tdl02-nav" aria-label="Collapse navigation" class="group/btn relative grid size-11 place-items-center rounded-xl text-tdl02-ink transition-colors hover:bg-tdl02-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tdl02-acc @max-[45rem]/shell:hidden">
        <svg viewBox="0 0 24 24" class="size-5 fill-none stroke-current stroke-2 [stroke-linecap:round]" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="M9 4v16"/></svg>
        <span aria-hidden="true" class="pointer-events-none absolute top-[calc(100%+6px)] left-0 z-20 flex items-center gap-1.5 rounded-lg bg-tdl02-ink px-2.5 py-1.5 text-xs font-semibold whitespace-nowrap text-white opacity-0 transition-opacity group-hover/btn:opacity-100 group-focus-visible/btn:opacity-100">Toggle sidebar<kbd class="rounded border border-white/25 px-1.5 font-sans text-[10px]">[</kbd></span>
      </button>
      <strong class="flex items-center gap-2.5 text-[15px] font-bold tracking-tight"><span aria-hidden="true" class="size-6 rounded-lg bg-[conic-gradient(from_200deg,var(--color-tdl02-acc),var(--color-tdl02-acc2),var(--color-tdl02-acc))]"></span>Ledgerly</strong>
      <label class="ml-auto flex h-10 w-[min(18rem,40%)] items-center gap-2 rounded-full border border-tdl02-edge bg-tdl02-bg px-3.5 text-[13px] text-tdl02-mut transition focus-within:border-tdl02-acc focus-within:ring-3 focus-within:ring-tdl02-acc/15 @max-[45rem]/shell:hidden">
        <svg viewBox="0 0 24 24" class="size-3.5 flex-none fill-none stroke-current stroke-2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
        <span class="sr-only">Search</span><input type="search" placeholder="Search orders, customers…" class="min-w-0 flex-1 bg-transparent text-tdl02-ink outline-none placeholder:text-tdl02-mut">
        <kbd class="rounded-md border border-tdl02-edge bg-tdl02-panel px-1.5 py-0.5 font-sans text-[10px] font-semibold">⌘K</kbd>
      </label>
      <span aria-hidden="true" class="grid size-9 place-items-center rounded-full bg-linear-140 from-tdl02-acc to-tdl02-acc3 text-[11px] font-bold text-white @max-[45rem]/shell:ml-auto">JD</span>
    </header>
    <div class="flex min-h-0 flex-1">
      <nav id="tdl02-nav" aria-label="Primary" class="relative z-10 flex w-59 flex-none flex-col gap-1 border-r border-tdl02-edge bg-tdl02-panel p-2.5 transition-[width] duration-300 ease-[cubic-bezier(.2,.7,.2,1)] group-data-collapsed/shell:w-17 @max-[45rem]/shell:w-17 [&>a]:relative [&>a]:flex [&>a]:h-11 [&>a]:items-center [&>a]:gap-3 [&>a]:rounded-xl [&>a]:px-3.25 [&>a]:text-[13.5px] [&>a]:font-semibold [&>a]:whitespace-nowrap [&>a]:text-tdl02-mut [&>a]:no-underline [&>a]:transition-colors [&>a:hover]:bg-tdl02-hover [&>a:hover]:text-tdl02-ink [&>a:focus-visible]:outline-2 [&>a:focus-visible]:-outline-offset-2 [&>a:focus-visible]:outline-tdl02-acc [&>a]:after:pointer-events-none [&>a]:after:absolute [&>a]:after:top-1/2 [&>a]:after:left-[calc(100%+12px)] [&>a]:after:-translate-y-1/2 [&>a]:after:scale-95 [&>a]:after:rounded-lg [&>a]:after:bg-tdl02-ink [&>a]:after:px-2.75 [&>a]:after:py-1.5 [&>a]:after:text-xs [&>a]:after:text-white [&>a]:after:opacity-0 [&>a]:after:shadow-lg [&>a]:after:transition [&>a]:after:content-[attr(data-label)] group-data-collapsed/shell:[&>a:hover]:after:scale-100 group-data-collapsed/shell:[&>a:hover]:after:opacity-100 group-data-collapsed/shell:[&>a:focus-visible]:after:opacity-100 @max-[45rem]/shell:[&>a:hover]:after:opacity-100 [&>a[aria-current=page]]:bg-tdl02-acc/10 [&>a[aria-current=page]]:text-tdl02-acc">
        <p class="px-3.25 pt-1 pb-1.5 text-[10.5px] font-bold tracking-[0.12em] whitespace-nowrap text-tdl02-mut uppercase transition-opacity group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Workspace</p>
        <a href="#overview" data-label="Overview" aria-current="page"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M3 12 12 4l9 8"/><path d="M5 10v10h14V10"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Overview</span></a>
        <a href="#orders" data-label="Orders · 12"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M4 7h16l-1.5 12h-13Z"/><path d="M8 7a4 4 0 0 1 8 0"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Orders</span><b class="ml-auto rounded-full bg-tdl02-acc px-2 py-0.5 text-[10.5px] text-white transition-opacity group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">12</b><i aria-hidden="true" class="absolute top-2.5 left-8 size-2 rounded-full bg-tdl02-acc opacity-0 ring-2 ring-tdl02-panel transition-opacity group-data-collapsed/shell:opacity-100 @max-[45rem]/shell:opacity-100"></i></a>
        <a href="#customers" data-label="Customers"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0"/><path d="M16 4.5a3.5 3.5 0 0 1 0 7M17.5 13.6a6.5 6.5 0 0 1 4 6.4"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Customers</span></a>
        <a href="#reports" data-label="Reports"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M5 20V10M12 20V4M19 20v-7"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Reports</span></a>
        <a href="#billing" data-label="Billing"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Billing</span></a>
        <span aria-hidden="true" class="flex-1"></span>
        <a href="#settings" data-label="Settings"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9 17 7M7 17l-2.1 2.1"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Settings</span></a>
      </nav>
      <main class="flex min-w-0 flex-1 flex-col gap-4 overflow-auto overscroll-contain p-4 sm:p-6">
        <div class="flex flex-wrap items-center justify-between gap-3">
          <div><h2 class="text-xl font-bold tracking-tight">Overview</h2><p class="text-[13px] text-tdl02-mut">Week of Sep 28 · all stores</p></div>
          <button type="button" class="flex h-11 items-center gap-2 rounded-xl bg-tdl02-acc px-4.5 text-[13px] font-semibold text-white shadow-[0_8px_20px_-8px_var(--color-tdl02-acc)] transition hover:bg-tdl02-acc/90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tdl02-acc active:translate-y-px"><svg viewBox="0 0 24 24" class="size-4 fill-none stroke-current stroke-[2.4] [stroke-linecap:round]" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>New order</button>
        </div>
        <div class="grid grid-cols-[repeat(auto-fit,minmax(min(11rem,100%),1fr))] gap-3.5">
          <article class="rounded-2xl border border-tdl02-edge bg-tdl02-panel px-4.5 py-4"><h3 class="text-[11px] font-semibold tracking-[0.1em] text-tdl02-mut uppercase">Revenue</h3><p class="mt-1.5 mb-0.5 text-2xl font-bold tracking-tight tabular-nums">$128,430</p><small class="text-xs font-semibold text-tdl02-good">▲ 8.2% vs last week</small></article>
          <article class="rounded-2xl border border-tdl02-edge bg-tdl02-panel px-4.5 py-4"><h3 class="text-[11px] font-semibold tracking-[0.1em] text-tdl02-mut uppercase">Orders</h3><p class="mt-1.5 mb-0.5 text-2xl font-bold tracking-tight tabular-nums">1,203</p><small class="text-xs font-semibold text-tdl02-good">▲ 3.1% vs last week</small></article>
          <article class="rounded-2xl border border-tdl02-edge bg-tdl02-panel px-4.5 py-4"><h3 class="text-[11px] font-semibold tracking-[0.1em] text-tdl02-mut uppercase">Refund rate</h3><p class="mt-1.5 mb-0.5 text-2xl font-bold tracking-tight tabular-nums">0.8%</p><small class="text-xs font-semibold text-tdl02-good">▼ 0.2 pts vs last week</small></article>
        </div>
        <section class="flex min-h-56 flex-1 flex-col gap-3 rounded-2xl border border-tdl02-edge bg-tdl02-panel p-4.5" aria-labelledby="tdl02-chart">
          <h3 id="tdl02-chart" class="flex justify-between text-[13px] font-semibold text-tdl02-mut">Sales this quarter<span class="text-tdl02-ink">$1.42M</span></h3>
          <div class="flex flex-1 items-end gap-2.5 border-b border-tdl02-edge">
            <div class="group/bar relative flex h-[35%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$98k</span></div>
            <div class="group/bar relative flex h-[52%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$146k</span></div>
            <div class="group/bar relative flex h-[44%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$123k</span></div>
            <div class="group/bar relative flex h-[61%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$171k</span></div>
            <div class="group/bar relative flex h-[57%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$160k</span></div>
            <div class="group/bar relative flex h-[73%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$204k</span></div>
            <div class="group/bar relative flex h-[66%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$185k</span></div>
            <div class="group/bar relative flex h-[84%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$235k</span></div>
            <div class="group/bar relative flex h-[92%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc transition"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$258k</span></div>
          </div>
          <p class="flex justify-between text-[11px] text-tdl02-mut"><span>Jul</span><span>Aug</span><span>Sep</span></p>
        </section>
      </main>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tdl02-bg: #f6f7fb;
  --color-tdl02-bg: oklch(0.975 0.005 265);
  --color-tdl02-panel: #ffffff;
  --color-tdl02-panel: oklch(1 0 0);
  --color-tdl02-hover: #eef0f6;
  --color-tdl02-hover: oklch(0.955 0.008 265);
  --color-tdl02-edge: #e2e4ec;
  --color-tdl02-edge: oklch(0.915 0.01 265);
  --color-tdl02-ink: #252a3a;
  --color-tdl02-ink: oklch(0.27 0.025 265);
  --color-tdl02-mut: #5d6375;
  --color-tdl02-mut: oklch(0.49 0.025 265);
  --color-tdl02-acc: #4651e0;
  --color-tdl02-acc: oklch(0.53 0.2 268);
  --color-tdl02-acc2: #3fb0d6;
  --color-tdl02-acc2: oklch(0.7 0.12 220);
  --color-tdl02-acc3: #9a5ae0;
  --color-tdl02-acc3: oklch(0.6 0.18 300);
  --color-tdl02-good: #16834a;
  --color-tdl02-good: oklch(0.53 0.13 155);
  --font-tdl02-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tdl-02 {
  width: 100%;
  min-height: 100svh;
}

@media (prefers-reduced-motion: reduce) {
  .tdl-02 * {
    transition-duration: 0.01s !important;
  }
}
(() => {
  document.querySelectorAll('.tdl-02 [data-shell]').forEach((shell) => {
    if (shell.dataset.bound) return; shell.dataset.bound = '1';
    const btn = shell.querySelector('[data-toggle]');
    const set = (collapsed) => {
      shell.toggleAttribute('data-collapsed', collapsed);
      btn.setAttribute('aria-expanded', String(!collapsed));
      btn.setAttribute('aria-label', collapsed ? 'Expand navigation' : 'Collapse navigation');
      try { localStorage.setItem('tdl02-collapsed', collapsed ? '1' : '0'); } catch (e) {}
    };
    let saved = null; try { saved = localStorage.getItem('tdl02-collapsed'); } catch (e) {}
    if (saved === '1') set(true);
    btn.addEventListener('click', () => set(!shell.hasAttribute('data-collapsed')));
    document.addEventListener('keydown', (e) => {
      if (e.key !== '[' || e.metaKey || e.ctrlKey || e.altKey) return;
      if (e.target.closest && e.target.closest('input, textarea, select, [contenteditable]')) return;
      set(!shell.hasAttribute('data-collapsed'));
    });
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

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

@theme {
  --color-tdl02-bg: #f6f7fb;
  --color-tdl02-bg: oklch(0.975 0.005 265);
  --color-tdl02-panel: #ffffff;
  --color-tdl02-panel: oklch(1 0 0);
  --color-tdl02-hover: #eef0f6;
  --color-tdl02-hover: oklch(0.955 0.008 265);
  --color-tdl02-edge: #e2e4ec;
  --color-tdl02-edge: oklch(0.915 0.01 265);
  --color-tdl02-ink: #252a3a;
  --color-tdl02-ink: oklch(0.27 0.025 265);
  --color-tdl02-mut: #5d6375;
  --color-tdl02-mut: oklch(0.49 0.025 265);
  --color-tdl02-acc: #4651e0;
  --color-tdl02-acc: oklch(0.53 0.2 268);
  --color-tdl02-acc2: #3fb0d6;
  --color-tdl02-acc2: oklch(0.7 0.12 220);
  --color-tdl02-acc3: #9a5ae0;
  --color-tdl02-acc3: oklch(0.6 0.18 300);
  --color-tdl02-good: #16834a;
  --color-tdl02-good: oklch(0.53 0.13 155);
  --font-tdl02-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tdl-02 {
  width: 100%;
  min-height: 100svh;
}

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

What changed in the port

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

  • [data-collapsed] .cdl-02__side, [data-collapsed] .cdl-02__label and the tooltip rules became group-data-collapsed/shell: variants on the elements themselves — the script still only sets an attribute.
  • The repeated link styles moved to the nav as [&>a]: child variants, so each link is just href, data-label and an icon.
  • Bug fix: the CSS original put overflow:hidden on the sidebar, which clipped its own rail tooltips. The nav is now overflow-visible z-10; tooltips render beside the rail.
  • UI upgrade: collapsed state persists across reloads (localStorage), [ toggles it, the toggle has its own tooltip, a dot badge replaces the hidden count on the rail, the search is a real labelled input with a focus ring, and chart bars reveal their values on hover.
  • UI upgrade: all hit targets are 44px (toggle was 40px); muted copy darkened from 0.55 to 0.49 lightness for 6.5:1.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind CSS Collapsible Sidebar Admin Dashboard
Source: https://codefronts.com/layouts/tailwind-css-dashboard-layouts/tailwind-css-collapsible-sidebar-admin-dashboard/

The canonical SaaS shell: top bar, a sidebar that folds from 236px to a 68px icon rail, and a fluid main area. The fold is w-59 → group-data-collapsed/shell:w-17, labels fade with group-data-collapsed/shell:opacity-0, and rail tooltips are generated by after:content-[attr(data-label)]. The script sets one attribute &mdash; it never touches a class &mdash; and now remembers the choice and answers the [ shortcut.
## HTML
```html
<section class="tdl-02 w-full min-h-svh bg-tdl02-bg font-tdl02-sans text-tdl02-ink antialiased" aria-label="Collapsible sidebar admin shell demo">
  <div data-shell class="group/shell @container/shell flex h-svh min-h-[34rem] flex-col">
    <header class="flex h-15 flex-none items-center gap-3 border-b border-tdl02-edge bg-tdl02-panel px-3 sm:px-4">
      <button data-toggle type="button" aria-expanded="true" aria-controls="tdl02-nav" aria-label="Collapse navigation" class="group/btn relative grid size-11 place-items-center rounded-xl text-tdl02-ink transition-colors hover:bg-tdl02-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tdl02-acc @max-[45rem]/shell:hidden">
        <svg viewBox="0 0 24 24" class="size-5 fill-none stroke-current stroke-2 [stroke-linecap:round]" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="3"/><path d="M9 4v16"/></svg>
        <span aria-hidden="true" class="pointer-events-none absolute top-[calc(100%+6px)] left-0 z-20 flex items-center gap-1.5 rounded-lg bg-tdl02-ink px-2.5 py-1.5 text-xs font-semibold whitespace-nowrap text-white opacity-0 transition-opacity group-hover/btn:opacity-100 group-focus-visible/btn:opacity-100">Toggle sidebar<kbd class="rounded border border-white/25 px-1.5 font-sans text-[10px]">[</kbd></span>
      </button>
      <strong class="flex items-center gap-2.5 text-[15px] font-bold tracking-tight"><span aria-hidden="true" class="size-6 rounded-lg bg-[conic-gradient(from_200deg,var(--color-tdl02-acc),var(--color-tdl02-acc2),var(--color-tdl02-acc))]"></span>Ledgerly</strong>
      <label class="ml-auto flex h-10 w-[min(18rem,40%)] items-center gap-2 rounded-full border border-tdl02-edge bg-tdl02-bg px-3.5 text-[13px] text-tdl02-mut transition focus-within:border-tdl02-acc focus-within:ring-3 focus-within:ring-tdl02-acc/15 @max-[45rem]/shell:hidden">
        <svg viewBox="0 0 24 24" class="size-3.5 flex-none fill-none stroke-current stroke-2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
        <span class="sr-only">Search</span><input type="search" placeholder="Search orders, customers…" class="min-w-0 flex-1 bg-transparent text-tdl02-ink outline-none placeholder:text-tdl02-mut">
        <kbd class="rounded-md border border-tdl02-edge bg-tdl02-panel px-1.5 py-0.5 font-sans text-[10px] font-semibold">⌘K</kbd>
      </label>
      <span aria-hidden="true" class="grid size-9 place-items-center rounded-full bg-linear-140 from-tdl02-acc to-tdl02-acc3 text-[11px] font-bold text-white @max-[45rem]/shell:ml-auto">JD</span>
    </header>
    <div class="flex min-h-0 flex-1">
      <nav id="tdl02-nav" aria-label="Primary" class="relative z-10 flex w-59 flex-none flex-col gap-1 border-r border-tdl02-edge bg-tdl02-panel p-2.5 transition-[width] duration-300 ease-[cubic-bezier(.2,.7,.2,1)] group-data-collapsed/shell:w-17 @max-[45rem]/shell:w-17 [&>a]:relative [&>a]:flex [&>a]:h-11 [&>a]:items-center [&>a]:gap-3 [&>a]:rounded-xl [&>a]:px-3.25 [&>a]:text-[13.5px] [&>a]:font-semibold [&>a]:whitespace-nowrap [&>a]:text-tdl02-mut [&>a]:no-underline [&>a]:transition-colors [&>a:hover]:bg-tdl02-hover [&>a:hover]:text-tdl02-ink [&>a:focus-visible]:outline-2 [&>a:focus-visible]:-outline-offset-2 [&>a:focus-visible]:outline-tdl02-acc [&>a]:after:pointer-events-none [&>a]:after:absolute [&>a]:after:top-1/2 [&>a]:after:left-[calc(100%+12px)] [&>a]:after:-translate-y-1/2 [&>a]:after:scale-95 [&>a]:after:rounded-lg [&>a]:after:bg-tdl02-ink [&>a]:after:px-2.75 [&>a]:after:py-1.5 [&>a]:after:text-xs [&>a]:after:text-white [&>a]:after:opacity-0 [&>a]:after:shadow-lg [&>a]:after:transition [&>a]:after:content-[attr(data-label)] group-data-collapsed/shell:[&>a:hover]:after:scale-100 group-data-collapsed/shell:[&>a:hover]:after:opacity-100 group-data-collapsed/shell:[&>a:focus-visible]:after:opacity-100 @max-[45rem]/shell:[&>a:hover]:after:opacity-100 [&>a[aria-current=page]]:bg-tdl02-acc/10 [&>a[aria-current=page]]:text-tdl02-acc">
        <p class="px-3.25 pt-1 pb-1.5 text-[10.5px] font-bold tracking-[0.12em] whitespace-nowrap text-tdl02-mut uppercase transition-opacity group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Workspace</p>
        <a href="#overview" data-label="Overview" aria-current="page"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M3 12 12 4l9 8"/><path d="M5 10v10h14V10"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Overview</span></a>
        <a href="#orders" data-label="Orders · 12"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M4 7h16l-1.5 12h-13Z"/><path d="M8 7a4 4 0 0 1 8 0"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Orders</span><b class="ml-auto rounded-full bg-tdl02-acc px-2 py-0.5 text-[10.5px] text-white transition-opacity group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">12</b><i aria-hidden="true" class="absolute top-2.5 left-8 size-2 rounded-full bg-tdl02-acc opacity-0 ring-2 ring-tdl02-panel transition-opacity group-data-collapsed/shell:opacity-100 @max-[45rem]/shell:opacity-100"></i></a>
        <a href="#customers" data-label="Customers"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0"/><path d="M16 4.5a3.5 3.5 0 0 1 0 7M17.5 13.6a6.5 6.5 0 0 1 4 6.4"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Customers</span></a>
        <a href="#reports" data-label="Reports"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><path d="M5 20V10M12 20V4M19 20v-7"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Reports</span></a>
        <a href="#billing" data-label="Billing"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Billing</span></a>
        <span aria-hidden="true" class="flex-1"></span>
        <a href="#settings" data-label="Settings"><svg viewBox="0 0 24 24" class="size-5 flex-none fill-none stroke-current stroke-[1.8] [stroke-linecap:round] [stroke-linejoin:round]" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3M4.9 4.9l2.1 2.1M17 17l2.1 2.1M19.1 4.9 17 7M7 17l-2.1 2.1"/></svg><span class="transition-[opacity,translate] duration-200 group-data-collapsed/shell:-translate-x-2 group-data-collapsed/shell:opacity-0 @max-[45rem]/shell:opacity-0">Settings</span></a>
      </nav>
      <main class="flex min-w-0 flex-1 flex-col gap-4 overflow-auto overscroll-contain p-4 sm:p-6">
        <div class="flex flex-wrap items-center justify-between gap-3">
          <div><h2 class="text-xl font-bold tracking-tight">Overview</h2><p class="text-[13px] text-tdl02-mut">Week of Sep 28 · all stores</p></div>
          <button type="button" class="flex h-11 items-center gap-2 rounded-xl bg-tdl02-acc px-4.5 text-[13px] font-semibold text-white shadow-[0_8px_20px_-8px_var(--color-tdl02-acc)] transition hover:bg-tdl02-acc/90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tdl02-acc active:translate-y-px"><svg viewBox="0 0 24 24" class="size-4 fill-none stroke-current stroke-[2.4] [stroke-linecap:round]" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>New order</button>
        </div>
        <div class="grid grid-cols-[repeat(auto-fit,minmax(min(11rem,100%),1fr))] gap-3.5">
          <article class="rounded-2xl border border-tdl02-edge bg-tdl02-panel px-4.5 py-4"><h3 class="text-[11px] font-semibold tracking-[0.1em] text-tdl02-mut uppercase">Revenue</h3><p class="mt-1.5 mb-0.5 text-2xl font-bold tracking-tight tabular-nums">$128,430</p><small class="text-xs font-semibold text-tdl02-good">▲ 8.2% vs last week</small></article>
          <article class="rounded-2xl border border-tdl02-edge bg-tdl02-panel px-4.5 py-4"><h3 class="text-[11px] font-semibold tracking-[0.1em] text-tdl02-mut uppercase">Orders</h3><p class="mt-1.5 mb-0.5 text-2xl font-bold tracking-tight tabular-nums">1,203</p><small class="text-xs font-semibold text-tdl02-good">▲ 3.1% vs last week</small></article>
          <article class="rounded-2xl border border-tdl02-edge bg-tdl02-panel px-4.5 py-4"><h3 class="text-[11px] font-semibold tracking-[0.1em] text-tdl02-mut uppercase">Refund rate</h3><p class="mt-1.5 mb-0.5 text-2xl font-bold tracking-tight tabular-nums">0.8%</p><small class="text-xs font-semibold text-tdl02-good">▼ 0.2 pts vs last week</small></article>
        </div>
        <section class="flex min-h-56 flex-1 flex-col gap-3 rounded-2xl border border-tdl02-edge bg-tdl02-panel p-4.5" aria-labelledby="tdl02-chart">
          <h3 id="tdl02-chart" class="flex justify-between text-[13px] font-semibold text-tdl02-mut">Sales this quarter<span class="text-tdl02-ink">$1.42M</span></h3>
          <div class="flex flex-1 items-end gap-2.5 border-b border-tdl02-edge">
            <div class="group/bar relative flex h-[35%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$98k</span></div>
            <div class="group/bar relative flex h-[52%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$146k</span></div>
            <div class="group/bar relative flex h-[44%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$123k</span></div>
            <div class="group/bar relative flex h-[61%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$171k</span></div>
            <div class="group/bar relative flex h-[57%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$160k</span></div>
            <div class="group/bar relative flex h-[73%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$204k</span></div>
            <div class="group/bar relative flex h-[66%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$185k</span></div>
            <div class="group/bar relative flex h-[84%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc/80 transition hover:to-tdl02-acc"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$235k</span></div>
            <div class="group/bar relative flex h-[92%] flex-1 justify-center rounded-t-lg bg-linear-to-t from-tdl02-acc/25 to-tdl02-acc transition"><span class="pointer-events-none absolute -top-8 rounded-md bg-tdl02-ink px-2 py-1 text-[11px] font-semibold text-white opacity-0 transition group-hover/bar:opacity-100">$258k</span></div>
          </div>
          <p class="flex justify-between text-[11px] text-tdl02-mut"><span>Jul</span><span>Aug</span><span>Sep</span></p>
        </section>
      </main>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tdl02-bg: #f6f7fb;
  --color-tdl02-bg: oklch(0.975 0.005 265);
  --color-tdl02-panel: #ffffff;
  --color-tdl02-panel: oklch(1 0 0);
  --color-tdl02-hover: #eef0f6;
  --color-tdl02-hover: oklch(0.955 0.008 265);
  --color-tdl02-edge: #e2e4ec;
  --color-tdl02-edge: oklch(0.915 0.01 265);
  --color-tdl02-ink: #252a3a;
  --color-tdl02-ink: oklch(0.27 0.025 265);
  --color-tdl02-mut: #5d6375;
  --color-tdl02-mut: oklch(0.49 0.025 265);
  --color-tdl02-acc: #4651e0;
  --color-tdl02-acc: oklch(0.53 0.2 268);
  --color-tdl02-acc2: #3fb0d6;
  --color-tdl02-acc2: oklch(0.7 0.12 220);
  --color-tdl02-acc3: #9a5ae0;
  --color-tdl02-acc3: oklch(0.6 0.18 300);
  --color-tdl02-good: #16834a;
  --color-tdl02-good: oklch(0.53 0.13 155);
  --font-tdl02-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
}

.tdl-02 {
  width: 100%;
  min-height: 100svh;
}

@media (prefers-reduced-motion: reduce) {
  .tdl-02 * {
    transition-duration: 0.01s !important;
  }
}
```

## JavaScript
```js
(() => {
  document.querySelectorAll('.tdl-02 [data-shell]').forEach((shell) => {
    if (shell.dataset.bound) return; shell.dataset.bound = '1';
    const btn = shell.querySelector('[data-toggle]');
    const set = (collapsed) => {
      shell.toggleAttribute('data-collapsed', collapsed);
      btn.setAttribute('aria-expanded', String(!collapsed));
      btn.setAttribute('aria-label', collapsed ? 'Expand navigation' : 'Collapse navigation');
      try { localStorage.setItem('tdl02-collapsed', collapsed ? '1' : '0'); } catch (e) {}
    };
    let saved = null; try { saved = localStorage.getItem('tdl02-collapsed'); } catch (e) {}
    if (saved === '1') set(true);
    btn.addEventListener('click', () => set(!shell.hasAttribute('data-collapsed')));
    document.addEventListener('keydown', (e) => {
      if (e.key !== '[' || e.metaKey || e.ctrlKey || e.altKey) return;
      if (e.target.closest && e.target.closest('input, textarea, select, [contenteditable]')) return;
      set(!shell.hasAttribute('data-collapsed'));
    });
  });
})();
```

How this works

One attribute, many variants. The script toggles data-collapsed on the shell; every element that changes reads it through a named group:

<div data-shell class="group/shell …">
  <nav class="w-59 transition-[width] group-data-collapsed/shell:w-17">
    <span class="group-data-collapsed/shell:opacity-0">Orders</span>

Tooltips only when they help. Each link carries data-label; its ::after is always generated but stays at opacity-0 until group-data-collapsed/shell:hover:after:opacity-100 (or focus-visible) matches — so expanded links never show a redundant tooltip.

Accessible disclosure. The toggle is a real <button aria-expanded aria-controls>; the script flips aria-expanded, swaps the label, persists the state to localStorage and binds the [ key (ignored while typing in a field).

Make it yours

  • Widths: w-59 (236px) and w-17 (68px) — any spacing step works in v4, no config.
  • Start collapsed: add data-collapsed to the shell and aria-expanded="false" to the button.
  • Shortcut: change the e.key === '[' check in the script, and the <kbd> hint in the toggle's tooltip.
  • Accent: edit --color-tdl02-acc; active link, badge, avatar, CTA and chart follow.

Gotchas — read before shipping

  • The nav must not be overflow-hidden or the rail tooltips are clipped (a bug in the CSS original). Labels are whitespace-nowrap + opacity-0 instead, so nothing visible overflows.
  • data-collapsed: matches the bare attribute; if you write data-collapsed="false" it still matches. Remove the attribute instead.
  • Transitioning width reflows the main column each frame; on very heavy pages switch to a grid-template-columns transition or a transform-based overlay.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Tailwind v4 floor. attr() in content is universal.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…