08 / 20
All 20 in 20 CSS Sidebar Navigation
- 01Accessible Sidebar Navigation ARIA Reference
- 02Sidebar Focus Trap and Restore on Close
- 03Inert Background Content Behind a Drawer
- 04Sidebar Navigation Is Not a Tab List
- 05Announcing Collapsed and Expanded Sidebar State
- 06Roving Tabindex for Sidebar Keyboard Navigation
- 07Persisting Sidebar Collapse State Across Reloads
- 08Sidebar Scroll Containment with Overscroll Behavior
- 09Nested Submenu Keyboard and Screen Reader Behavior
- 10Resizable Sidebar with a Keyboard Accessible Handle
- 11Collapsible Sidebar with Checkbox State
- 12Responsive Sidebar Breakpoint Behavior
- 13Icon Rail Sidebar with Accessible Labels
- 14Dashboard Sidebar with Current Page State
- 15Multi Level Sidebar Menu Disclosure Pattern
- 16Accordion Sidebar with Details and Summary
- 17Sticky Full Height Sidebar Layout Mechanics
- 18Floating Sidebar Stacking and Overlay Order
- 19Off Canvas Sidebar Drawer with Focus Return
- 20Sidebar Footer Pinning with Scrollable Nav
Sidebar Scroll Containment with Overscroll Behavior
Scroll to the bottom of a long sidebar and keep going, and the scroll chains to the page behind it — on an open drawer that means the background moves while the drawer sits still. overscroll-behavior: contain stops the chain in one declaration. The rail here contains its own scroll, and the thread beside it does the same. Includes the related iOS problem, where the body still scrolls behind a fixed overlay, and what actually fixes it.
Mosaic by Cruip: a complete Tailwind admin dashboard template in HTML, React, Next.js and Vue · $69
Cruip · Mosaic by Cruip · Affiliate link
The code
<section class="sdb-08" aria-label="Talon chat">
<div class="sdb-08__theme">
<input class="sdb-08__theme-input" type="checkbox" id="sdb-08-theme">
<label class="sdb-08__theme-label" for="sdb-08-theme">
<svg 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="sdb-08__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-08__app">
<nav class="sdb-08__rail" aria-label="Talon channels">
<p class="sdb-08__rail-title">Channels</p>
<ul class="sdb-08__list">
<li><a class="sdb-08__link" href="#sdb-08-thread" aria-current="page">#deployments</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#incidents</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#billing</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#design-review</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#hiring</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#audit-log</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#api-keys</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#field-notes</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#vendor-desk</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#support-escalations</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#release-notes</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#retro</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#status-page</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#certificates</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#on-call</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#runbooks</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#postmortems</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#capacity</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#data-platform</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#mobile</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#design-system</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#localisation</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#partners</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#recruiting</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#social</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#random</a></li>
</ul>
</nav>
<main class="sdb-08__thread" id="sdb-08-thread">
<p class="sdb-08__channel">#deployments</p>
<p class="sdb-08__msg"><b>Priya Sharma</b> Rollback finished, pointer is back on 4c19f.</p>
<p class="sdb-08__msg"><b>Jordan Lee</b> Confirmed on the status page.</p>
<p class="sdb-08__msg"><b>Sam Rivera</b> Retention window is 90 days now — runbook updated.</p>
<p class="sdb-08__msg"><b>Ada Okafor</b> Token exchange p99 is back under 200ms.</p>
<p class="sdb-08__msg"><b>Tomas Vidal</b> Staging snapshot is rebuilding, ETA 10 minutes.</p>
<p class="sdb-08__msg"><b>Priya Sharma</b> Nice. I'll close the incident once staging is green.</p>
</main>
</div>
</section><section class="sdb-08" aria-label="Talon chat">
<div class="sdb-08__theme">
<input class="sdb-08__theme-input" type="checkbox" id="sdb-08-theme">
<label class="sdb-08__theme-label" for="sdb-08-theme">
<svg 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="sdb-08__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-08__app">
<nav class="sdb-08__rail" aria-label="Talon channels">
<p class="sdb-08__rail-title">Channels</p>
<ul class="sdb-08__list">
<li><a class="sdb-08__link" href="#sdb-08-thread" aria-current="page">#deployments</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#incidents</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#billing</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#design-review</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#hiring</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#audit-log</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#api-keys</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#field-notes</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#vendor-desk</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#support-escalations</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#release-notes</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#retro</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#status-page</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#certificates</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#on-call</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#runbooks</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#postmortems</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#capacity</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#data-platform</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#mobile</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#design-system</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#localisation</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#partners</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#recruiting</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#social</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#random</a></li>
</ul>
</nav>
<main class="sdb-08__thread" id="sdb-08-thread">
<p class="sdb-08__channel">#deployments</p>
<p class="sdb-08__msg"><b>Priya Sharma</b> Rollback finished, pointer is back on 4c19f.</p>
<p class="sdb-08__msg"><b>Jordan Lee</b> Confirmed on the status page.</p>
<p class="sdb-08__msg"><b>Sam Rivera</b> Retention window is 90 days now — runbook updated.</p>
<p class="sdb-08__msg"><b>Ada Okafor</b> Token exchange p99 is back under 200ms.</p>
<p class="sdb-08__msg"><b>Tomas Vidal</b> Staging snapshot is rebuilding, ETA 10 minutes.</p>
<p class="sdb-08__msg"><b>Priya Sharma</b> Nice. I'll close the incident once staging is green.</p>
</main>
</div>
</section>.sdb-08 {
--sdb-08-bg: #211d19;
--sdb-08-surf: #2b2521;
--sdb-08-surf2: #362e28;
--sdb-08-line: #453b33;
--sdb-08-ink: #f4ece3;
--sdb-08-mut: #b3a496;
--sdb-08-acc: #e0a13c;
--sdb-08-acc: oklch(76% 0.14 72);
--sdb-08-acc-soft: #3d3021;
--sdb-08-font-display: Rockwell, "Bookman Old Style", "Roboto Slab", Georgia, serif;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-08-bg);
color: var(--sdb-08-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-08 *,
.sdb-08 *::before,
.sdb-08 *::after {
box-sizing: border-box;
}
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
@media (prefers-color-scheme: light) {
.sdb-08 {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #211d19;
--sdb-08-surf: #2b2521;
--sdb-08-surf2: #362e28;
--sdb-08-line: #453b33;
--sdb-08-ink: #f4ece3;
--sdb-08-mut: #b3a496;
--sdb-08-acc: #e0a13c;
--sdb-08-acc: oklch(76% 0.14 72);
--sdb-08-acc-soft: #3d3021;
}
}
@media (prefers-color-scheme: dark) {
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
}
.sdb-08__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-08__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-08__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 2px solid var(--sdb-08-ink);
background: var(--sdb-08-surf);
color: var(--sdb-08-ink);
box-shadow: 4px 4px 0 var(--sdb-08-ink);
cursor: pointer;
}
.sdb-08__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-08__theme-input:focus-visible + .sdb-08__theme-label {
outline: 2px solid var(--sdb-08-acc);
outline-offset: 3px;
}
.sdb-08__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-08__app {
display: flex;
min-inline-size: 0;
block-size: 100vh;
background: var(--sdb-08-surf);
}
.sdb-08__rail {
flex: none;
inline-size: 13rem;
min-inline-size: 0;
min-block-size: 0;
display: flex;
flex-direction: column;
border-inline-end: 2px solid var(--sdb-08-ink);
background: var(--sdb-08-surf2);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.sdb-08__rail-title {
position: sticky;
inset-block-start: 0;
margin: 0;
padding: 0.6rem 0.7rem;
z-index: 2;
background: var(--sdb-08-surf2);
border-block-end: 1px solid var(--sdb-08-line);
font-family: var(--sdb-08-font-display);
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sdb-08-mut);
}
.sdb-08__list {
list-style: none;
margin: 0;
padding: 0.25rem;
}
.sdb-08__link {
display: flex;
align-items: center;
min-block-size: 2.75rem;
padding: 0.4rem 0.55rem;
color: var(--sdb-08-ink);
text-decoration: none;
font-size: 0.88rem;
min-inline-size: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.sdb-08__link:hover {
background: var(--sdb-08-acc-soft);
}
.sdb-08__link[aria-current="page"] {
background: var(--sdb-08-acc);
color: var(--sdb-08-bg);
font-weight: 700;
}
.sdb-08__link:focus-visible {
outline: 2px solid var(--sdb-08-acc);
outline-offset: -2px;
}
.sdb-08__thread {
flex: 1;
min-inline-size: 0;
min-block-size: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: 1.1rem 1.25rem 2rem;
display: grid;
gap: 0.75rem;
align-content: start;
}
@media (min-width: 46rem) {
.sdb-08__thread {
padding: 1.6rem 2rem 2.5rem;
}
}
.sdb-08__channel {
margin: 0 0 0.4rem;
font-family: var(--sdb-08-font-display);
font-size: 1.15rem;
letter-spacing: 0.02em;
color: var(--sdb-08-acc);
}
.sdb-08__msg {
margin: 0;
font-size: 0.92rem;
color: var(--sdb-08-mut);
max-inline-size: 44rem;
text-wrap: pretty;
}
.sdb-08__msg b {
color: var(--sdb-08-ink);
display: block;
font-size: 0.82rem;
letter-spacing: 0.02em;
}.sdb-08 {
--sdb-08-bg: #211d19;
--sdb-08-surf: #2b2521;
--sdb-08-surf2: #362e28;
--sdb-08-line: #453b33;
--sdb-08-ink: #f4ece3;
--sdb-08-mut: #b3a496;
--sdb-08-acc: #e0a13c;
--sdb-08-acc: oklch(76% 0.14 72);
--sdb-08-acc-soft: #3d3021;
--sdb-08-font-display: Rockwell, "Bookman Old Style", "Roboto Slab", Georgia, serif;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-08-bg);
color: var(--sdb-08-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-08 *,
.sdb-08 *::before,
.sdb-08 *::after {
box-sizing: border-box;
}
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
@media (prefers-color-scheme: light) {
.sdb-08 {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #211d19;
--sdb-08-surf: #2b2521;
--sdb-08-surf2: #362e28;
--sdb-08-line: #453b33;
--sdb-08-ink: #f4ece3;
--sdb-08-mut: #b3a496;
--sdb-08-acc: #e0a13c;
--sdb-08-acc: oklch(76% 0.14 72);
--sdb-08-acc-soft: #3d3021;
}
}
@media (prefers-color-scheme: dark) {
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
}
.sdb-08__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-08__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-08__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 2px solid var(--sdb-08-ink);
background: var(--sdb-08-surf);
color: var(--sdb-08-ink);
box-shadow: 4px 4px 0 var(--sdb-08-ink);
cursor: pointer;
}
.sdb-08__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-08__theme-input:focus-visible + .sdb-08__theme-label {
outline: 2px solid var(--sdb-08-acc);
outline-offset: 3px;
}
.sdb-08__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-08__app {
display: flex;
min-inline-size: 0;
block-size: 100vh;
background: var(--sdb-08-surf);
}
.sdb-08__rail {
flex: none;
inline-size: 13rem;
min-inline-size: 0;
min-block-size: 0;
display: flex;
flex-direction: column;
border-inline-end: 2px solid var(--sdb-08-ink);
background: var(--sdb-08-surf2);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.sdb-08__rail-title {
position: sticky;
inset-block-start: 0;
margin: 0;
padding: 0.6rem 0.7rem;
z-index: 2;
background: var(--sdb-08-surf2);
border-block-end: 1px solid var(--sdb-08-line);
font-family: var(--sdb-08-font-display);
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sdb-08-mut);
}
.sdb-08__list {
list-style: none;
margin: 0;
padding: 0.25rem;
}
.sdb-08__link {
display: flex;
align-items: center;
min-block-size: 2.75rem;
padding: 0.4rem 0.55rem;
color: var(--sdb-08-ink);
text-decoration: none;
font-size: 0.88rem;
min-inline-size: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.sdb-08__link:hover {
background: var(--sdb-08-acc-soft);
}
.sdb-08__link[aria-current="page"] {
background: var(--sdb-08-acc);
color: var(--sdb-08-bg);
font-weight: 700;
}
.sdb-08__link:focus-visible {
outline: 2px solid var(--sdb-08-acc);
outline-offset: -2px;
}
.sdb-08__thread {
flex: 1;
min-inline-size: 0;
min-block-size: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: 1.1rem 1.25rem 2rem;
display: grid;
gap: 0.75rem;
align-content: start;
}
@media (min-width: 46rem) {
.sdb-08__thread {
padding: 1.6rem 2rem 2.5rem;
}
}
.sdb-08__channel {
margin: 0 0 0.4rem;
font-family: var(--sdb-08-font-display);
font-size: 1.15rem;
letter-spacing: 0.02em;
color: var(--sdb-08-acc);
}
.sdb-08__msg {
margin: 0;
font-size: 0.92rem;
color: var(--sdb-08-mut);
max-inline-size: 44rem;
text-wrap: pretty;
}
.sdb-08__msg b {
color: var(--sdb-08-ink);
display: block;
font-size: 0.82rem;
letter-spacing: 0.02em;
}Here's a working CSS Sidebar Navigation 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: Sidebar Scroll Containment with Overscroll Behavior
Source: https://codefronts.com/navigation/css-sidebar-navigation/sidebar-scroll-containment-with-overscroll-behavior/
Scroll to the bottom of a long sidebar and keep going, and the scroll chains to the page behind it — on an open drawer that means the background moves while the drawer sits still. overscroll-behavior: contain stops the chain in one declaration. The rail here contains its own scroll, and the thread beside it does the same. Includes the related iOS problem, where the body still scrolls behind a fixed overlay, and what actually fixes it.
## HTML
```html
<section class="sdb-08" aria-label="Talon chat">
<div class="sdb-08__theme">
<input class="sdb-08__theme-input" type="checkbox" id="sdb-08-theme">
<label class="sdb-08__theme-label" for="sdb-08-theme">
<svg 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="sdb-08__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-08__app">
<nav class="sdb-08__rail" aria-label="Talon channels">
<p class="sdb-08__rail-title">Channels</p>
<ul class="sdb-08__list">
<li><a class="sdb-08__link" href="#sdb-08-thread" aria-current="page">#deployments</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#incidents</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#billing</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#design-review</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#hiring</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#audit-log</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#api-keys</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#field-notes</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#vendor-desk</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#support-escalations</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#release-notes</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#retro</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#status-page</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#certificates</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#on-call</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#runbooks</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#postmortems</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#capacity</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#data-platform</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#mobile</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#design-system</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#localisation</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#partners</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#recruiting</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#social</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#random</a></li>
</ul>
</nav>
<main class="sdb-08__thread" id="sdb-08-thread">
<p class="sdb-08__channel">#deployments</p>
<p class="sdb-08__msg"><b>Priya Sharma</b> Rollback finished, pointer is back on 4c19f.</p>
<p class="sdb-08__msg"><b>Jordan Lee</b> Confirmed on the status page.</p>
<p class="sdb-08__msg"><b>Sam Rivera</b> Retention window is 90 days now — runbook updated.</p>
<p class="sdb-08__msg"><b>Ada Okafor</b> Token exchange p99 is back under 200ms.</p>
<p class="sdb-08__msg"><b>Tomas Vidal</b> Staging snapshot is rebuilding, ETA 10 minutes.</p>
<p class="sdb-08__msg"><b>Priya Sharma</b> Nice. I'll close the incident once staging is green.</p>
</main>
</div>
</section>
```
## CSS
```css
.sdb-08 {
--sdb-08-bg: #211d19;
--sdb-08-surf: #2b2521;
--sdb-08-surf2: #362e28;
--sdb-08-line: #453b33;
--sdb-08-ink: #f4ece3;
--sdb-08-mut: #b3a496;
--sdb-08-acc: #e0a13c;
--sdb-08-acc: oklch(76% 0.14 72);
--sdb-08-acc-soft: #3d3021;
--sdb-08-font-display: Rockwell, "Bookman Old Style", "Roboto Slab", Georgia, serif;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-08-bg);
color: var(--sdb-08-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-08 *,
.sdb-08 *::before,
.sdb-08 *::after {
box-sizing: border-box;
}
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
@media (prefers-color-scheme: light) {
.sdb-08 {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #211d19;
--sdb-08-surf: #2b2521;
--sdb-08-surf2: #362e28;
--sdb-08-line: #453b33;
--sdb-08-ink: #f4ece3;
--sdb-08-mut: #b3a496;
--sdb-08-acc: #e0a13c;
--sdb-08-acc: oklch(76% 0.14 72);
--sdb-08-acc-soft: #3d3021;
}
}
@media (prefers-color-scheme: dark) {
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
}
.sdb-08__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-08__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-08__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 2px solid var(--sdb-08-ink);
background: var(--sdb-08-surf);
color: var(--sdb-08-ink);
box-shadow: 4px 4px 0 var(--sdb-08-ink);
cursor: pointer;
}
.sdb-08__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-08__theme-input:focus-visible + .sdb-08__theme-label {
outline: 2px solid var(--sdb-08-acc);
outline-offset: 3px;
}
.sdb-08__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-08__app {
display: flex;
min-inline-size: 0;
block-size: 100vh;
background: var(--sdb-08-surf);
}
.sdb-08__rail {
flex: none;
inline-size: 13rem;
min-inline-size: 0;
min-block-size: 0;
display: flex;
flex-direction: column;
border-inline-end: 2px solid var(--sdb-08-ink);
background: var(--sdb-08-surf2);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.sdb-08__rail-title {
position: sticky;
inset-block-start: 0;
margin: 0;
padding: 0.6rem 0.7rem;
z-index: 2;
background: var(--sdb-08-surf2);
border-block-end: 1px solid var(--sdb-08-line);
font-family: var(--sdb-08-font-display);
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sdb-08-mut);
}
.sdb-08__list {
list-style: none;
margin: 0;
padding: 0.25rem;
}
.sdb-08__link {
display: flex;
align-items: center;
min-block-size: 2.75rem;
padding: 0.4rem 0.55rem;
color: var(--sdb-08-ink);
text-decoration: none;
font-size: 0.88rem;
min-inline-size: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.sdb-08__link:hover {
background: var(--sdb-08-acc-soft);
}
.sdb-08__link[aria-current="page"] {
background: var(--sdb-08-acc);
color: var(--sdb-08-bg);
font-weight: 700;
}
.sdb-08__link:focus-visible {
outline: 2px solid var(--sdb-08-acc);
outline-offset: -2px;
}
.sdb-08__thread {
flex: 1;
min-inline-size: 0;
min-block-size: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: 1.1rem 1.25rem 2rem;
display: grid;
gap: 0.75rem;
align-content: start;
}
@media (min-width: 46rem) {
.sdb-08__thread {
padding: 1.6rem 2rem 2.5rem;
}
}
.sdb-08__channel {
margin: 0 0 0.4rem;
font-family: var(--sdb-08-font-display);
font-size: 1.15rem;
letter-spacing: 0.02em;
color: var(--sdb-08-acc);
}
.sdb-08__msg {
margin: 0;
font-size: 0.92rem;
color: var(--sdb-08-mut);
max-inline-size: 44rem;
text-wrap: pretty;
}
.sdb-08__msg b {
color: var(--sdb-08-ink);
display: block;
font-size: 0.82rem;
letter-spacing: 0.02em;
}
```Here's a working CSS Sidebar Navigation 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: Sidebar Scroll Containment with Overscroll Behavior
Source: https://codefronts.com/navigation/css-sidebar-navigation/sidebar-scroll-containment-with-overscroll-behavior/
Scroll to the bottom of a long sidebar and keep going, and the scroll chains to the page behind it — on an open drawer that means the background moves while the drawer sits still. overscroll-behavior: contain stops the chain in one declaration. The rail here contains its own scroll, and the thread beside it does the same. Includes the related iOS problem, where the body still scrolls behind a fixed overlay, and what actually fixes it.
## HTML
```html
<section class="sdb-08" aria-label="Talon chat">
<div class="sdb-08__theme">
<input class="sdb-08__theme-input" type="checkbox" id="sdb-08-theme">
<label class="sdb-08__theme-label" for="sdb-08-theme">
<svg 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="sdb-08__sr">Toggle dark theme</span>
</label>
</div>
<div class="sdb-08__app">
<nav class="sdb-08__rail" aria-label="Talon channels">
<p class="sdb-08__rail-title">Channels</p>
<ul class="sdb-08__list">
<li><a class="sdb-08__link" href="#sdb-08-thread" aria-current="page">#deployments</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#incidents</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#billing</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#design-review</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#hiring</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#audit-log</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#api-keys</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#field-notes</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#vendor-desk</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#support-escalations</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#release-notes</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#retro</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#status-page</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#certificates</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#on-call</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#runbooks</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#postmortems</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#capacity</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#data-platform</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#mobile</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#design-system</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#localisation</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#partners</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#recruiting</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#social</a></li>
<li><a class="sdb-08__link" href="#sdb-08-thread">#random</a></li>
</ul>
</nav>
<main class="sdb-08__thread" id="sdb-08-thread">
<p class="sdb-08__channel">#deployments</p>
<p class="sdb-08__msg"><b>Priya Sharma</b> Rollback finished, pointer is back on 4c19f.</p>
<p class="sdb-08__msg"><b>Jordan Lee</b> Confirmed on the status page.</p>
<p class="sdb-08__msg"><b>Sam Rivera</b> Retention window is 90 days now — runbook updated.</p>
<p class="sdb-08__msg"><b>Ada Okafor</b> Token exchange p99 is back under 200ms.</p>
<p class="sdb-08__msg"><b>Tomas Vidal</b> Staging snapshot is rebuilding, ETA 10 minutes.</p>
<p class="sdb-08__msg"><b>Priya Sharma</b> Nice. I'll close the incident once staging is green.</p>
</main>
</div>
</section>
```
## CSS
```css
.sdb-08 {
--sdb-08-bg: #211d19;
--sdb-08-surf: #2b2521;
--sdb-08-surf2: #362e28;
--sdb-08-line: #453b33;
--sdb-08-ink: #f4ece3;
--sdb-08-mut: #b3a496;
--sdb-08-acc: #e0a13c;
--sdb-08-acc: oklch(76% 0.14 72);
--sdb-08-acc-soft: #3d3021;
--sdb-08-font-display: Rockwell, "Bookman Old Style", "Roboto Slab", Georgia, serif;
width: 100%;
min-height: 100vh;
display: block;
position: relative;
box-sizing: border-box;
background: var(--sdb-08-bg);
color: var(--sdb-08-ink);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.55;
}
.sdb-08 *,
.sdb-08 *::before,
.sdb-08 *::after {
box-sizing: border-box;
}
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
@media (prefers-color-scheme: light) {
.sdb-08 {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #211d19;
--sdb-08-surf: #2b2521;
--sdb-08-surf2: #362e28;
--sdb-08-line: #453b33;
--sdb-08-ink: #f4ece3;
--sdb-08-mut: #b3a496;
--sdb-08-acc: #e0a13c;
--sdb-08-acc: oklch(76% 0.14 72);
--sdb-08-acc-soft: #3d3021;
}
}
@media (prefers-color-scheme: dark) {
.sdb-08:has(.sdb-08__theme-input:checked) {
--sdb-08-bg: #f7f2ea;
--sdb-08-surf: #ffffff;
--sdb-08-surf2: #efe7dc;
--sdb-08-line: #ddd0c0;
--sdb-08-ink: #241d17;
--sdb-08-mut: #6c5c4e;
--sdb-08-acc: #8a5210;
--sdb-08-acc: oklch(48% 0.11 68);
--sdb-08-acc-soft: #f2e3cf;
}
}
.sdb-08__theme {
position: absolute;
inset-block-start: 0.85rem;
inset-inline-end: 0.85rem;
z-index: 60;
}
.sdb-08__theme-input {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.sdb-08__theme-label {
display: grid;
place-items: center;
inline-size: 2.75rem;
block-size: 2.75rem;
border: 2px solid var(--sdb-08-ink);
background: var(--sdb-08-surf);
color: var(--sdb-08-ink);
box-shadow: 4px 4px 0 var(--sdb-08-ink);
cursor: pointer;
}
.sdb-08__theme-label svg {
inline-size: 1.15rem;
block-size: 1.15rem;
}
.sdb-08__theme-input:focus-visible + .sdb-08__theme-label {
outline: 2px solid var(--sdb-08-acc);
outline-offset: 3px;
}
.sdb-08__sr {
position: absolute;
inline-size: 1px;
block-size: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.sdb-08__app {
display: flex;
min-inline-size: 0;
block-size: 100vh;
background: var(--sdb-08-surf);
}
.sdb-08__rail {
flex: none;
inline-size: 13rem;
min-inline-size: 0;
min-block-size: 0;
display: flex;
flex-direction: column;
border-inline-end: 2px solid var(--sdb-08-ink);
background: var(--sdb-08-surf2);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.sdb-08__rail-title {
position: sticky;
inset-block-start: 0;
margin: 0;
padding: 0.6rem 0.7rem;
z-index: 2;
background: var(--sdb-08-surf2);
border-block-end: 1px solid var(--sdb-08-line);
font-family: var(--sdb-08-font-display);
font-size: 0.78rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--sdb-08-mut);
}
.sdb-08__list {
list-style: none;
margin: 0;
padding: 0.25rem;
}
.sdb-08__link {
display: flex;
align-items: center;
min-block-size: 2.75rem;
padding: 0.4rem 0.55rem;
color: var(--sdb-08-ink);
text-decoration: none;
font-size: 0.88rem;
min-inline-size: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.sdb-08__link:hover {
background: var(--sdb-08-acc-soft);
}
.sdb-08__link[aria-current="page"] {
background: var(--sdb-08-acc);
color: var(--sdb-08-bg);
font-weight: 700;
}
.sdb-08__link:focus-visible {
outline: 2px solid var(--sdb-08-acc);
outline-offset: -2px;
}
.sdb-08__thread {
flex: 1;
min-inline-size: 0;
min-block-size: 0;
overflow-y: auto;
overscroll-behavior: contain;
padding: 1.1rem 1.25rem 2rem;
display: grid;
gap: 0.75rem;
align-content: start;
}
@media (min-width: 46rem) {
.sdb-08__thread {
padding: 1.6rem 2rem 2.5rem;
}
}
.sdb-08__channel {
margin: 0 0 0.4rem;
font-family: var(--sdb-08-font-display);
font-size: 1.15rem;
letter-spacing: 0.02em;
color: var(--sdb-08-acc);
}
.sdb-08__msg {
margin: 0;
font-size: 0.92rem;
color: var(--sdb-08-mut);
max-inline-size: 44rem;
text-wrap: pretty;
}
.sdb-08__msg b {
color: var(--sdb-08-ink);
display: block;
font-size: 0.82rem;
letter-spacing: 0.02em;
}
```How this works
Scroll chaining is the browser's default and it is usually right. When a scrollable element reaches its end, further scrolling passes to its nearest scrollable ancestor. That is what lets you flick through a page containing an embedded list without getting stuck. In a sidebar it is wrong: reaching the bottom of the channel list and continuing scrolls the conversation behind it, so the user's context moves while they were looking at something else.
overscroll-behavior: contain is the whole fix. Applied to the scroll container, it keeps the scroll inside that element when it reaches the boundary, while leaving the platform's own overscroll affordance — the rubber-band bounce — intact. none goes further and suppresses the bounce too, which is worth it for a full-screen drawer and is heavy-handed for a rail inside a page. Prefer contain unless you have a reason.
Two layout mechanics have to be right before the declaration can matter. The scroll container needs a bounded height — in a flex column that means min-block-size: 0 on the scrolling child, because its default min-height: auto lets it grow to its content and it never scrolls at all. And the overflow must be on the element you think it is on; a scrollbar that has quietly landed on an ancestor makes overscroll-behavior on the child do nothing.
The iOS caveat is separate and older. overscroll-behavior only shipped in Safari 16, and even with it, a fixed overlay over a scrolled document can still let the body move underneath on touch. The reliable fix is to stop the body scrolling while the drawer is open — store window.scrollY, set the body to position: fixed with a matching negative top, and restore both on close. It is ugly, and it is still the thing that works on old iOS.
Make it yours
- Switch
containtononeon the drawer variant if the overscroll bounce reads as sloppy on a full-height panel. - Add
overscroll-behavior-blockinstead of the shorthand when you want to contain vertical chaining but leave horizontal alone. - Remove
scrollbar-gutter: stableif your rail has no hover-revealed scrollbar; keep it if the list reflows when the scrollbar appears. - Make the group headers non-sticky by dropping one rule — sticky headers make the boundary more obvious, which is why they are here.
- Give the scroll container a
scroll-padding-block-endso the last item is not flush against the boundary when it comes to rest. - Apply the same declaration to a main-content pane that scrolls independently; chaining is symmetric and the fix is identical.
Gotchas — read before shipping
- A flex column child without
min-block-size: 0never scrolls, becausemin-height: autolets it grow to its content. The declaration then appears to do nothing, and the real bug is the layout. - Chaining behind an open drawer is the worst version of this: the user loses their place in content they cannot see moving, and closing the drawer reveals a page scrolled somewhere else entirely.
overscroll-behavioronly reached Safari in 16, so iOS below that ignores it. The body-scroll-lock workaround is still needed if you support those versions.- Putting the declaration on the wrong element is easy when overflow has crept onto an ancestor. Check which element actually has the scrollbar before assuming the property is broken.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by :has(), oklch(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 63+.
overscroll-behavior sets the floor and Safari shipped it late, in 16 — iOS below that chains regardless, so a drawer supporting old iOS still needs the body-scroll-lock workaround described in the notes. The theme toggle needs :has() (Chrome 105 / Safari 15.4 / Firefox 121) and falls back to the operating-system scheme; oklch() values are declared after sRGB hex fallbacks throughout.