Pure TailwindMIT licensed

Tailwind Sidebar Scroll Containment with Overscroll Behavior

Two panes, two scrollbars, and the bug everyone has felt: you reach the bottom of the channel list and the page starts scrolling instead. overscroll-behavior: contain fixes it in one declaration — which Tailwind has no utility for, correctly — so this demo is the collection's clearest case for arbitrary properties, alongside the min-h-0 that makes an overflowing flex child possible at all.

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="tsb-08 block relative w-full min-h-svh bg-tsb08-bg text-tsb08-ink font-tsb08-sans leading-[1.55]" aria-label="Talon chat">
  <div class="absolute top-3.5 right-3.5 z-60">
    <input class="tsb-08-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-08-theme">
    <label class="grid place-items-center w-11 h-11 border-2 border-tsb08-ink bg-tsb08-surf text-tsb08-ink cursor-pointer shadow-[4px_4px_0_var(--color-tsb08-ink)] peer-focus-visible:outline-2 peer-focus-visible:outline-tsb08-acc peer-focus-visible:outline-offset-3" for="tsb-08-theme">
      <svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
    </label>
  </div>

  <div class="flex min-w-0 h-svh bg-tsb08-surf">
    <nav class="flex-none flex flex-col w-52 min-w-0 min-h-0 overflow-y-auto bg-tsb08-surf2 border-r-2 border-tsb08-ink [overscroll-behavior:contain] [scrollbar-gutter:stable]" aria-label="Talon channels">
      <p class="sticky top-0 z-2 m-0 px-2.5 py-2.5 bg-tsb08-surf2 border-b border-tsb08-line font-tsb08-display text-[0.78rem] tracking-[0.08em] uppercase text-tsb08-mut">Channels</p>
      <ul class="list-none m-0 p-1 [&_a]:flex [&_a]:items-center [&_a]:min-w-0 [&_a]:min-h-11 [&_a]:px-2 [&_a]:py-1.5 [&_a]:font-tsb08-mono [&_a]:text-[0.88rem] [&_a]:text-tsb08-ink [&_a]:no-underline [&_a:hover]:bg-tsb08-accsoft [&_a:focus-visible]:outline-2 [&_a:focus-visible]:outline-tsb08-acc [&_a:focus-visible]:-outline-offset-2 [&_a[aria-current=page]]:bg-tsb08-acc [&_a[aria-current=page]]:text-tsb08-bg [&_a[aria-current=page]]:font-bold">
        <li><a href="#tsb-08-thread" aria-current="page">#deployments</a></li>
        <li><a href="#tsb-08-thread">#incidents</a></li>
        <li><a href="#tsb-08-thread">#billing</a></li>
        <li><a href="#tsb-08-thread">#design-review</a></li>
        <li><a href="#tsb-08-thread">#hiring</a></li>
        <li><a href="#tsb-08-thread">#audit-log</a></li>
        <li><a href="#tsb-08-thread">#api-keys</a></li>
        <li><a href="#tsb-08-thread">#field-notes</a></li>
        <li><a href="#tsb-08-thread">#vendor-desk</a></li>
        <li><a href="#tsb-08-thread">#support-escalations</a></li>
        <li><a href="#tsb-08-thread">#release-notes</a></li>
        <li><a href="#tsb-08-thread">#retro</a></li>
        <li><a href="#tsb-08-thread">#status-page</a></li>
        <li><a href="#tsb-08-thread">#certificates</a></li>
        <li><a href="#tsb-08-thread">#on-call</a></li>
        <li><a href="#tsb-08-thread">#runbooks</a></li>
        <li><a href="#tsb-08-thread">#postmortems</a></li>
        <li><a href="#tsb-08-thread">#capacity</a></li>
        <li><a href="#tsb-08-thread">#data-platform</a></li>
        <li><a href="#tsb-08-thread">#mobile</a></li>
        <li><a href="#tsb-08-thread">#design-system</a></li>
        <li><a href="#tsb-08-thread">#localisation</a></li>
        <li><a href="#tsb-08-thread">#partners</a></li>
        <li><a href="#tsb-08-thread">#recruiting</a></li>
        <li><a href="#tsb-08-thread">#social</a></li>
        <li><a href="#tsb-08-thread">#random</a></li>
      </ul>
    </nav>

    <main class="flex-1 min-w-0 min-h-0 overflow-y-auto grid gap-3 content-start px-5 pt-4.5 pb-8 [overscroll-behavior:contain] r46:px-8 r46:pt-6.5 r46:pb-10" id="tsb-08-thread">
      <p class="m-0 mb-1.5 font-tsb08-display text-[1.15rem] tracking-[0.02em] text-tsb08-acc">#deployments</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Priya Sharma</b> Rollback finished, pointer is back on 4c19f.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Jordan Lee</b> Confirmed on the status page.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Sam Rivera</b> Retention window is 90 days now &mdash; runbook updated.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Ada Okafor</b> Token exchange p99 is back under 200ms.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Tomas Vidal</b> Staging snapshot is rebuilding, ETA 10 minutes.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Priya Sharma</b> Nice. I'll close the incident once staging is green.</p>
    </main>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tsb08-bg: #211d19;
  --color-tsb08-bg: oklch(24% 0.012 60);
  --color-tsb08-surf: #2b2521;
  --color-tsb08-surf: oklch(29% 0.014 60);
  --color-tsb08-surf2: #362e28;
  --color-tsb08-surf2: oklch(34% 0.016 60);
  --color-tsb08-line: #453b33;
  --color-tsb08-line: oklch(40% 0.018 60);
  --color-tsb08-ink: #f4ece3;
  --color-tsb08-ink: oklch(95% 0.014 80);
  --color-tsb08-mut: #b3a496;
  --color-tsb08-mut: oklch(73% 0.02 75);
  --color-tsb08-acc: #e0a13c;
  --color-tsb08-acc: oklch(76% 0.14 72);
  --color-tsb08-accsoft: #3d3021;
  --color-tsb08-accsoft: oklch(33% 0.03 70);
  --font-tsb08-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-tsb08-display: 'Roboto Slab', Rockwell, 'Bookman Old Style', Georgia, serif;
  --font-tsb08-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh. */

.tsb-08 {
  width: 100%;
  min-height: 100svh;
}

.tsb-08 *,
.tsb-08 *::before,
.tsb-08 *::after {
  box-sizing: border-box;
}
/* This demo is dark by default, so the flip is inverted: the @theme block above IS the
   dark palette, and the light mirror lives in prefers-color-scheme: light. */

.tsb-08:has(.tsb-08-theme-input:checked) {
  --color-tsb08-bg: #f7f2ea;
  --color-tsb08-surf: #ffffff;
  --color-tsb08-surf2: #efe7dc;
  --color-tsb08-line: #ddd0c0;
  --color-tsb08-ink: #241d17;
  --color-tsb08-mut: #6c5c4e;
  --color-tsb08-acc: #8a5210;
  --color-tsb08-acc: oklch(48% 0.11 68);
  --color-tsb08-accsoft: #f2e3cf;
}

@media (prefers-color-scheme: light) {
  .tsb-08 {
    --color-tsb08-bg: #f7f2ea;
    --color-tsb08-surf: #ffffff;
    --color-tsb08-surf2: #efe7dc;
    --color-tsb08-line: #ddd0c0;
    --color-tsb08-ink: #241d17;
    --color-tsb08-mut: #6c5c4e;
    --color-tsb08-acc: #8a5210;
    --color-tsb08-acc: oklch(48% 0.11 68);
    --color-tsb08-accsoft: #f2e3cf;
  }

  .tsb-08:has(.tsb-08-theme-input:checked) {
    --color-tsb08-bg: #211d19;
    --color-tsb08-surf: #2b2521;
    --color-tsb08-surf2: #362e28;
    --color-tsb08-line: #453b33;
    --color-tsb08-ink: #f4ece3;
    --color-tsb08-mut: #b3a496;
    --color-tsb08-acc: #e0a13c;
    --color-tsb08-acc: oklch(76% 0.14 72);
    --color-tsb08-accsoft: #3d3021;
  }
}

@media (prefers-color-scheme: dark) {
  .tsb-08:has(.tsb-08-theme-input:checked) {
    --color-tsb08-bg: #f7f2ea;
    --color-tsb08-surf: #ffffff;
    --color-tsb08-surf2: #efe7dc;
    --color-tsb08-line: #ddd0c0;
    --color-tsb08-ink: #241d17;
    --color-tsb08-mut: #6c5c4e;
    --color-tsb08-acc: #8a5210;
    --color-tsb08-acc: oklch(48% 0.11 68);
    --color-tsb08-accsoft: #f2e3cf;
  }
}

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-tsb08-bg: #211d19;
  --color-tsb08-bg: oklch(24% 0.012 60);
  --color-tsb08-surf: #2b2521;
  --color-tsb08-surf: oklch(29% 0.014 60);
  --color-tsb08-surf2: #362e28;
  --color-tsb08-surf2: oklch(34% 0.016 60);
  --color-tsb08-line: #453b33;
  --color-tsb08-line: oklch(40% 0.018 60);
  --color-tsb08-ink: #f4ece3;
  --color-tsb08-ink: oklch(95% 0.014 80);
  --color-tsb08-mut: #b3a496;
  --color-tsb08-mut: oklch(73% 0.02 75);
  --color-tsb08-acc: #e0a13c;
  --color-tsb08-acc: oklch(76% 0.14 72);
  --color-tsb08-accsoft: #3d3021;
  --color-tsb08-accsoft: oklch(33% 0.03 70);
  --font-tsb08-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-tsb08-display: 'Roboto Slab', Rockwell, 'Bookman Old Style', Georgia, serif;
  --font-tsb08-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh. */

.tsb-08 {
  width: 100%;
  min-height: 100svh;
}

.tsb-08 *,
.tsb-08 *::before,
.tsb-08 *::after {
  box-sizing: border-box;
}
/* This demo is dark by default, so the flip is inverted: the @theme block above IS the
   dark palette, and the light mirror lives in prefers-color-scheme: light. */

.tsb-08:has(.tsb-08-theme-input:checked) {
  --color-tsb08-bg: #f7f2ea;
  --color-tsb08-surf: #ffffff;
  --color-tsb08-surf2: #efe7dc;
  --color-tsb08-line: #ddd0c0;
  --color-tsb08-ink: #241d17;
  --color-tsb08-mut: #6c5c4e;
  --color-tsb08-acc: #8a5210;
  --color-tsb08-acc: oklch(48% 0.11 68);
  --color-tsb08-accsoft: #f2e3cf;
}

@media (prefers-color-scheme: light) {
  .tsb-08 {
    --color-tsb08-bg: #f7f2ea;
    --color-tsb08-surf: #ffffff;
    --color-tsb08-surf2: #efe7dc;
    --color-tsb08-line: #ddd0c0;
    --color-tsb08-ink: #241d17;
    --color-tsb08-mut: #6c5c4e;
    --color-tsb08-acc: #8a5210;
    --color-tsb08-acc: oklch(48% 0.11 68);
    --color-tsb08-accsoft: #f2e3cf;
  }

  .tsb-08:has(.tsb-08-theme-input:checked) {
    --color-tsb08-bg: #211d19;
    --color-tsb08-surf: #2b2521;
    --color-tsb08-surf2: #362e28;
    --color-tsb08-line: #453b33;
    --color-tsb08-ink: #f4ece3;
    --color-tsb08-mut: #b3a496;
    --color-tsb08-acc: #e0a13c;
    --color-tsb08-acc: oklch(76% 0.14 72);
    --color-tsb08-accsoft: #3d3021;
  }
}

@media (prefers-color-scheme: dark) {
  .tsb-08:has(.tsb-08-theme-input:checked) {
    --color-tsb08-bg: #f7f2ea;
    --color-tsb08-surf: #ffffff;
    --color-tsb08-surf2: #efe7dc;
    --color-tsb08-line: #ddd0c0;
    --color-tsb08-ink: #241d17;
    --color-tsb08-mut: #6c5c4e;
    --color-tsb08-acc: #8a5210;
    --color-tsb08-acc: oklch(48% 0.11 68);
    --color-tsb08-accsoft: #f2e3cf;
  }
}
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=Roboto+Slab:wght@400;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap">

What changed in the port

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

  • overscroll-behavior and scrollbar-gutter are the collection's best argument for arbitrary properties: single correct structural answers, not design-scale values, and clearer as [overscroll-behavior:contain] than as an invented class name.
  • Twenty-six identical links would be twenty-six identical class lists, so this demo uses descendant arbitrary variants on the <ul> instead — [&_a]:, [&_a:hover]:, [&_a[aria-current=page]]:. That is the honest port of a .list a rule, and the one place in the collection where utilities on the parent beat utilities on the child.
  • min-h-0 is the least obvious utility in the whole set. It is what the CSS version wrote as min-block-size: 0, and it is the reason the panes scroll rather than the page.
  • The sticky section title keeps bg-tsb08-surf2 and z-2 in the class list, right next to sticky top-0 — in CSS those three lines are easy to separate and then lose.
  • The hard-offset shadow ports as shadow-[4px_4px_0_var(--color-tsb08-ink)], reading the same ink token as the 2px borders.
  • The dark-first token flip stays in the stylesheet, with the light palette in the prefers-color-scheme: light block — the mirror structure is the same as the rest of the collection, just inverted.
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 Sidebar Scroll Containment with Overscroll Behavior
Source: https://codefronts.com/navigation/tailwind-css-sidebars/tailwind-sidebar-scroll-containment-with-overscroll-behavior/

Two panes, two scrollbars, and the bug everyone has felt: you reach the bottom of the channel list and the page starts scrolling instead. overscroll-behavior: contain fixes it in one declaration &mdash; which Tailwind has no utility for, correctly &mdash; so this demo is the collection's clearest case for arbitrary properties, alongside the min-h-0 that makes an overflowing flex child possible at all.
## HTML
```html
<section class="tsb-08 block relative w-full min-h-svh bg-tsb08-bg text-tsb08-ink font-tsb08-sans leading-[1.55]" aria-label="Talon chat">
  <div class="absolute top-3.5 right-3.5 z-60">
    <input class="tsb-08-theme-input peer absolute inset-0 w-full h-full m-0 opacity-0 cursor-pointer" type="checkbox" id="tsb-08-theme">
    <label class="grid place-items-center w-11 h-11 border-2 border-tsb08-ink bg-tsb08-surf text-tsb08-ink cursor-pointer shadow-[4px_4px_0_var(--color-tsb08-ink)] peer-focus-visible:outline-2 peer-focus-visible:outline-tsb08-acc peer-focus-visible:outline-offset-3" for="tsb-08-theme">
      <svg class="w-4.5 h-4.5" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="absolute w-px h-px overflow-hidden whitespace-nowrap [clip-path:inset(50%)]">Toggle dark theme</span>
    </label>
  </div>

  <div class="flex min-w-0 h-svh bg-tsb08-surf">
    <nav class="flex-none flex flex-col w-52 min-w-0 min-h-0 overflow-y-auto bg-tsb08-surf2 border-r-2 border-tsb08-ink [overscroll-behavior:contain] [scrollbar-gutter:stable]" aria-label="Talon channels">
      <p class="sticky top-0 z-2 m-0 px-2.5 py-2.5 bg-tsb08-surf2 border-b border-tsb08-line font-tsb08-display text-[0.78rem] tracking-[0.08em] uppercase text-tsb08-mut">Channels</p>
      <ul class="list-none m-0 p-1 [&_a]:flex [&_a]:items-center [&_a]:min-w-0 [&_a]:min-h-11 [&_a]:px-2 [&_a]:py-1.5 [&_a]:font-tsb08-mono [&_a]:text-[0.88rem] [&_a]:text-tsb08-ink [&_a]:no-underline [&_a:hover]:bg-tsb08-accsoft [&_a:focus-visible]:outline-2 [&_a:focus-visible]:outline-tsb08-acc [&_a:focus-visible]:-outline-offset-2 [&_a[aria-current=page]]:bg-tsb08-acc [&_a[aria-current=page]]:text-tsb08-bg [&_a[aria-current=page]]:font-bold">
        <li><a href="#tsb-08-thread" aria-current="page">#deployments</a></li>
        <li><a href="#tsb-08-thread">#incidents</a></li>
        <li><a href="#tsb-08-thread">#billing</a></li>
        <li><a href="#tsb-08-thread">#design-review</a></li>
        <li><a href="#tsb-08-thread">#hiring</a></li>
        <li><a href="#tsb-08-thread">#audit-log</a></li>
        <li><a href="#tsb-08-thread">#api-keys</a></li>
        <li><a href="#tsb-08-thread">#field-notes</a></li>
        <li><a href="#tsb-08-thread">#vendor-desk</a></li>
        <li><a href="#tsb-08-thread">#support-escalations</a></li>
        <li><a href="#tsb-08-thread">#release-notes</a></li>
        <li><a href="#tsb-08-thread">#retro</a></li>
        <li><a href="#tsb-08-thread">#status-page</a></li>
        <li><a href="#tsb-08-thread">#certificates</a></li>
        <li><a href="#tsb-08-thread">#on-call</a></li>
        <li><a href="#tsb-08-thread">#runbooks</a></li>
        <li><a href="#tsb-08-thread">#postmortems</a></li>
        <li><a href="#tsb-08-thread">#capacity</a></li>
        <li><a href="#tsb-08-thread">#data-platform</a></li>
        <li><a href="#tsb-08-thread">#mobile</a></li>
        <li><a href="#tsb-08-thread">#design-system</a></li>
        <li><a href="#tsb-08-thread">#localisation</a></li>
        <li><a href="#tsb-08-thread">#partners</a></li>
        <li><a href="#tsb-08-thread">#recruiting</a></li>
        <li><a href="#tsb-08-thread">#social</a></li>
        <li><a href="#tsb-08-thread">#random</a></li>
      </ul>
    </nav>

    <main class="flex-1 min-w-0 min-h-0 overflow-y-auto grid gap-3 content-start px-5 pt-4.5 pb-8 [overscroll-behavior:contain] r46:px-8 r46:pt-6.5 r46:pb-10" id="tsb-08-thread">
      <p class="m-0 mb-1.5 font-tsb08-display text-[1.15rem] tracking-[0.02em] text-tsb08-acc">#deployments</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Priya Sharma</b> Rollback finished, pointer is back on 4c19f.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Jordan Lee</b> Confirmed on the status page.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Sam Rivera</b> Retention window is 90 days now &mdash; runbook updated.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Ada Okafor</b> Token exchange p99 is back under 200ms.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Tomas Vidal</b> Staging snapshot is rebuilding, ETA 10 minutes.</p>
      <p class="m-0 max-w-[44rem] text-[0.92rem] text-tsb08-mut text-pretty"><b class="block text-[0.82rem] tracking-[0.02em] text-tsb08-ink">Priya Sharma</b> Nice. I'll close the incident once staging is green.</p>
    </main>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tsb08-bg: #211d19;
  --color-tsb08-bg: oklch(24% 0.012 60);
  --color-tsb08-surf: #2b2521;
  --color-tsb08-surf: oklch(29% 0.014 60);
  --color-tsb08-surf2: #362e28;
  --color-tsb08-surf2: oklch(34% 0.016 60);
  --color-tsb08-line: #453b33;
  --color-tsb08-line: oklch(40% 0.018 60);
  --color-tsb08-ink: #f4ece3;
  --color-tsb08-ink: oklch(95% 0.014 80);
  --color-tsb08-mut: #b3a496;
  --color-tsb08-mut: oklch(73% 0.02 75);
  --color-tsb08-acc: #e0a13c;
  --color-tsb08-acc: oklch(76% 0.14 72);
  --color-tsb08-accsoft: #3d3021;
  --color-tsb08-accsoft: oklch(33% 0.03 70);
  --font-tsb08-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-tsb08-display: 'Roboto Slab', Rockwell, 'Bookman Old Style', Georgia, serif;
  --font-tsb08-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --breakpoint-r46: 46rem;
}
/* Root rule: the full-bleed contract, stated in CSS as well as in the markup's
   w-full min-h-svh. */

.tsb-08 {
  width: 100%;
  min-height: 100svh;
}

.tsb-08 *,
.tsb-08 *::before,
.tsb-08 *::after {
  box-sizing: border-box;
}
/* This demo is dark by default, so the flip is inverted: the @theme block above IS the
   dark palette, and the light mirror lives in prefers-color-scheme: light. */

.tsb-08:has(.tsb-08-theme-input:checked) {
  --color-tsb08-bg: #f7f2ea;
  --color-tsb08-surf: #ffffff;
  --color-tsb08-surf2: #efe7dc;
  --color-tsb08-line: #ddd0c0;
  --color-tsb08-ink: #241d17;
  --color-tsb08-mut: #6c5c4e;
  --color-tsb08-acc: #8a5210;
  --color-tsb08-acc: oklch(48% 0.11 68);
  --color-tsb08-accsoft: #f2e3cf;
}

@media (prefers-color-scheme: light) {
  .tsb-08 {
    --color-tsb08-bg: #f7f2ea;
    --color-tsb08-surf: #ffffff;
    --color-tsb08-surf2: #efe7dc;
    --color-tsb08-line: #ddd0c0;
    --color-tsb08-ink: #241d17;
    --color-tsb08-mut: #6c5c4e;
    --color-tsb08-acc: #8a5210;
    --color-tsb08-acc: oklch(48% 0.11 68);
    --color-tsb08-accsoft: #f2e3cf;
  }

  .tsb-08:has(.tsb-08-theme-input:checked) {
    --color-tsb08-bg: #211d19;
    --color-tsb08-surf: #2b2521;
    --color-tsb08-surf2: #362e28;
    --color-tsb08-line: #453b33;
    --color-tsb08-ink: #f4ece3;
    --color-tsb08-mut: #b3a496;
    --color-tsb08-acc: #e0a13c;
    --color-tsb08-acc: oklch(76% 0.14 72);
    --color-tsb08-accsoft: #3d3021;
  }
}

@media (prefers-color-scheme: dark) {
  .tsb-08:has(.tsb-08-theme-input:checked) {
    --color-tsb08-bg: #f7f2ea;
    --color-tsb08-surf: #ffffff;
    --color-tsb08-surf2: #efe7dc;
    --color-tsb08-line: #ddd0c0;
    --color-tsb08-ink: #241d17;
    --color-tsb08-mut: #6c5c4e;
    --color-tsb08-acc: #8a5210;
    --color-tsb08-acc: oklch(48% 0.11 68);
    --color-tsb08-accsoft: #f2e3cf;
  }
}
```

How this works

Three utilities make an independent scroll pane, and two of them are easy to forget. overflow-y-auto is the obvious one. min-h-0 is what lets a flex child shrink below its content height — without it the rail grows to fit twenty-six channels and the whole page scrolls instead. [overscroll-behavior:contain] is what stops a flick that reaches the end of the list from continuing into the page behind.

<nav class="flex-none w-52 min-h-0 overflow-y-auto
     [overscroll-behavior:contain]
     [scrollbar-gutter:stable]">

Why Tailwind should not ship these as utilities. A utility scale earns its place when the values are a design vocabulary — spacing, colour, radius. contain and stable are single correct answers to structural questions, and an arbitrary property says so more clearly than a memorised class name would.

The sticky title is sticky to the pane, not the page. Because the rail is the scroll container, sticky top-0 pins the “Channels” heading to the top of the list. It needs an opaque background and a z- above the links, or the items scroll through it.

This demo is dark by default. The token flip is inverted relative to the rest of the collection: the base palette is the dark one and the prefers-color-scheme: light block provides the light mirror.

Make it yours

  • Swap [overscroll-behavior:contain] for none to also kill pull-to-refresh and the rubber-band effect on the pane.
  • Rail width is the single w-52 on the nav.
  • Drop the sticky title by deleting sticky top-0 z-2; the containment is independent of it.
  • Retheme through --color-tsb08-acc and --color-tsb08-surf2; the demo is deliberately dark-first, and the light mirror lives in the stylesheet.
  • Soften the brutalist edges by replacing border-2 and the hard shadow with border and shadow-sm.
  • For a page-level version of this, put the containment on the <body> scroller instead; the pattern is identical.

Gotchas — read before shipping

  • Without min-h-0 the flex child refuses to shrink and overflow-y-auto never engages. It is the most common reason &ldquo;my sidebar scrolls the page&rdquo;.
  • h-screen on the app shell is doing real work here: a scroll pane needs a bounded height, and min-h-svh alone does not bound it.
  • A sticky header inside a scroll pane needs an opaque background. With a transparent one the list scrolls visibly through the text.
  • overscroll-behavior must go on the SCROLLING element, not its parent. On the wrapper it does nothing.
  • [scrollbar-gutter:stable] only helps where scrollbars take layout space; on overlay-scrollbar platforms it is a no-op, which is fine.
  • Remember the thread pane needs the same three utilities. Containing only the sidebar fixes half the problem.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

overscroll-behavior is Baseline 2022 and scrollbar-gutter is Baseline 2024 (Safari 17.4) — on older Safari the gutter utility is ignored and the list shifts slightly when the scrollbar appears, with no other consequence. Tailwind v4's own floor is higher than the containment's.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Theme tokensset in @theme, used by a utility

HTML & JSno Tailwind involved

Search CodeFronts

Loading…