30 CSS Sidebar Layouts03 / 30

Pure CSSMIT licensed

Developer Documentation Sidebar with Scroll-Synced Nesting

The Stripe-style docs tree: three levels of nested navigation with depth-driven indentation from a single --depth custom property, a vertical guide rail connecting each level, and the current section lit up automatically via :target as readers jump between anchors — no scroll-spy JavaScript.

Published

Live Demo
Try it

The code

<section class="sbl-04">
  <aside class="sbl-04__rail">
    <nav aria-label="Documentation">
      <ul class="sbl-04__tree">
        <li><a href="#sbl04-start">Getting started</a>
          <ul>
            <li><a href="#sbl04-install">Installation</a></li>
            <li><a href="#sbl04-auth">Authentication</a>
              <ul><li><a href="#sbl04-keys">API keys</a></li><li><a href="#sbl04-oauth">OAuth 2.0</a></li></ul>
            </li>
          </ul>
        </li>
        <li><a href="#sbl04-core">Core concepts</a>
          <ul><li><a href="#sbl04-webhooks">Webhooks</a></li><li><a href="#sbl04-errors">Error handling</a></li></ul>
        </li>
      </ul>
    </nav>
  </aside>
  <main class="sbl-04__doc">
    <section id="sbl04-start"><h1>Getting started</h1><p>Click any sidebar item — the matching link lights via :target, no scroll-spy script.</p></section>
    <section id="sbl04-install"><h2>Installation</h2><p>Install the SDK with your package manager of choice.</p></section>
    <section id="sbl04-auth"><h2>Authentication</h2><p>All requests are authenticated with a bearer token.</p></section>
    <section id="sbl04-keys"><h3>API keys</h3><p>Generate keys from the dashboard. Rotate them quarterly.</p></section>
    <section id="sbl04-oauth"><h3>OAuth 2.0</h3><p>Use the authorization-code flow for user-scoped access.</p></section>
    <section id="sbl04-core"><h2>Core concepts</h2><p>Resources, events and idempotency.</p></section>
    <section id="sbl04-webhooks"><h3>Webhooks</h3><p>Receive events at your endpoint within seconds.</p></section>
    <section id="sbl04-errors"><h3>Error handling</h3><p>Every error returns a typed code and a human message.</p></section>
  </main>
</section>
.sbl-04,
.sbl-04 *,
.sbl-04 *::before,
.sbl-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sbl-04 {
  --ink: oklch(0.27 0.015 265);
  --mut: oklch(0.52 0.012 265);
  --line: oklch(0.9 0.007 265);
  --accent: oklch(0.55 0.17 265);
  display: grid;
  grid-template-columns: 250px 1fr;
  height: 100vh;
  height: 100dvh;
  font-family: system-ui,'Segoe UI',sans-serif;
  background: oklch(0.99 0.002 100);
  color: var(--ink);
}

.sbl-04__rail {
  border-right: 1px solid var(--line);
  padding: 24px 12px;
  overflow-y: auto;
}

.sbl-04__tree {
  --depth: 0;
  list-style: none;
}

.sbl-04__tree ul {
  --depth: 1;
  list-style: none;
  border-inline-start: 1px solid var(--line);
  margin-inline-start: calc(12px + (var(--depth) - 1) * 16px + 7px);
}

.sbl-04__tree ul ul {
  --depth: 2;
  margin-inline-start: 15px;
}

.sbl-04__tree a {
  display: block;
  padding: 7px 10px 7px calc(12px + var(--depth) * 14px);
  margin: 1px 0;
  border-radius: 8px;
  font: 500 13.5px/1.3 system-ui,sans-serif;
  color: var(--mut);
  text-decoration: none;
  transition: color .15s ease,background-color .15s ease;
}

.sbl-04__tree ul a {
  padding-inline-start: 12px;
}

.sbl-04__tree a:hover {
  color: var(--ink);
  background: color-mix(in oklab,var(--ink) 5%,transparent);
}

.sbl-04__tree a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.sbl-04:has(#sbl04-start:target) a[href="#sbl04-start"],
.sbl-04:has(#sbl04-install:target) a[href="#sbl04-install"],
.sbl-04:has(#sbl04-auth:target) a[href="#sbl04-auth"],
.sbl-04:has(#sbl04-keys:target) a[href="#sbl04-keys"],
.sbl-04:has(#sbl04-oauth:target) a[href="#sbl04-oauth"],
.sbl-04:has(#sbl04-core:target) a[href="#sbl04-core"],
.sbl-04:has(#sbl04-webhooks:target) a[href="#sbl04-webhooks"],
.sbl-04:has(#sbl04-errors:target) a[href="#sbl04-errors"] {
  color: var(--accent);
  background: color-mix(in oklab,var(--accent) 10%,transparent);
  font-weight: 600;
}

.sbl-04__doc {
  overflow-y: auto;
  scroll-behavior: smooth;
  padding: 36px 44px;
  display: grid;
  gap: 34px;
  align-content: start;
}

.sbl-04__doc section {
  scroll-margin-top: 24px;
}

.sbl-04__doc h1 {
  font: 700 26px/1.2 system-ui,sans-serif;
}

.sbl-04__doc h2 {
  font: 650 20px/1.25 system-ui,sans-serif;
}

.sbl-04__doc h3 {
  font: 600 16px/1.3 system-ui,sans-serif;
}

.sbl-04__doc p {
  margin-top: 8px;
  color: var(--mut);
  font-size: 14px;
  line-height: 1.65;
  max-width: 58ch;
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .sbl-04__doc {
    scroll-behavior: auto;
  }

  .sbl-04__tree a {
    transition: none;
  }
}
/* No JavaScript — indentation derives from one --depth variable per nesting level; the active link lights via .sbl-04:has(#id:target) a[href="#id"]. */
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 Sidebar Layout 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: Developer Documentation Sidebar with Scroll-Synced Nesting
Source: https://codefronts.com/layouts/css-sidebar-layouts/developer-documentation-sidebar-with-scroll-synced-nesting/

The Stripe-style docs tree: three levels of nested navigation with depth-driven indentation from a single --depth custom property, a vertical guide rail connecting each level, and the current section lit up automatically via :target as readers jump between anchors — no scroll-spy JavaScript.
## HTML
```html
<section class="sbl-04">
  <aside class="sbl-04__rail">
    <nav aria-label="Documentation">
      <ul class="sbl-04__tree">
        <li><a href="#sbl04-start">Getting started</a>
          <ul>
            <li><a href="#sbl04-install">Installation</a></li>
            <li><a href="#sbl04-auth">Authentication</a>
              <ul><li><a href="#sbl04-keys">API keys</a></li><li><a href="#sbl04-oauth">OAuth 2.0</a></li></ul>
            </li>
          </ul>
        </li>
        <li><a href="#sbl04-core">Core concepts</a>
          <ul><li><a href="#sbl04-webhooks">Webhooks</a></li><li><a href="#sbl04-errors">Error handling</a></li></ul>
        </li>
      </ul>
    </nav>
  </aside>
  <main class="sbl-04__doc">
    <section id="sbl04-start"><h1>Getting started</h1><p>Click any sidebar item — the matching link lights via :target, no scroll-spy script.</p></section>
    <section id="sbl04-install"><h2>Installation</h2><p>Install the SDK with your package manager of choice.</p></section>
    <section id="sbl04-auth"><h2>Authentication</h2><p>All requests are authenticated with a bearer token.</p></section>
    <section id="sbl04-keys"><h3>API keys</h3><p>Generate keys from the dashboard. Rotate them quarterly.</p></section>
    <section id="sbl04-oauth"><h3>OAuth 2.0</h3><p>Use the authorization-code flow for user-scoped access.</p></section>
    <section id="sbl04-core"><h2>Core concepts</h2><p>Resources, events and idempotency.</p></section>
    <section id="sbl04-webhooks"><h3>Webhooks</h3><p>Receive events at your endpoint within seconds.</p></section>
    <section id="sbl04-errors"><h3>Error handling</h3><p>Every error returns a typed code and a human message.</p></section>
  </main>
</section>
```
## CSS
```css
.sbl-04,
.sbl-04 *,
.sbl-04 *::before,
.sbl-04 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sbl-04 {
  --ink: oklch(0.27 0.015 265);
  --mut: oklch(0.52 0.012 265);
  --line: oklch(0.9 0.007 265);
  --accent: oklch(0.55 0.17 265);
  display: grid;
  grid-template-columns: 250px 1fr;
  height: 100vh;
  height: 100dvh;
  font-family: system-ui,'Segoe UI',sans-serif;
  background: oklch(0.99 0.002 100);
  color: var(--ink);
}

.sbl-04__rail {
  border-right: 1px solid var(--line);
  padding: 24px 12px;
  overflow-y: auto;
}

.sbl-04__tree {
  --depth: 0;
  list-style: none;
}

.sbl-04__tree ul {
  --depth: 1;
  list-style: none;
  border-inline-start: 1px solid var(--line);
  margin-inline-start: calc(12px + (var(--depth) - 1) * 16px + 7px);
}

.sbl-04__tree ul ul {
  --depth: 2;
  margin-inline-start: 15px;
}

.sbl-04__tree a {
  display: block;
  padding: 7px 10px 7px calc(12px + var(--depth) * 14px);
  margin: 1px 0;
  border-radius: 8px;
  font: 500 13.5px/1.3 system-ui,sans-serif;
  color: var(--mut);
  text-decoration: none;
  transition: color .15s ease,background-color .15s ease;
}

.sbl-04__tree ul a {
  padding-inline-start: 12px;
}

.sbl-04__tree a:hover {
  color: var(--ink);
  background: color-mix(in oklab,var(--ink) 5%,transparent);
}

.sbl-04__tree a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.sbl-04:has(#sbl04-start:target) a[href="#sbl04-start"],
.sbl-04:has(#sbl04-install:target) a[href="#sbl04-install"],
.sbl-04:has(#sbl04-auth:target) a[href="#sbl04-auth"],
.sbl-04:has(#sbl04-keys:target) a[href="#sbl04-keys"],
.sbl-04:has(#sbl04-oauth:target) a[href="#sbl04-oauth"],
.sbl-04:has(#sbl04-core:target) a[href="#sbl04-core"],
.sbl-04:has(#sbl04-webhooks:target) a[href="#sbl04-webhooks"],
.sbl-04:has(#sbl04-errors:target) a[href="#sbl04-errors"] {
  color: var(--accent);
  background: color-mix(in oklab,var(--accent) 10%,transparent);
  font-weight: 600;
}

.sbl-04__doc {
  overflow-y: auto;
  scroll-behavior: smooth;
  padding: 36px 44px;
  display: grid;
  gap: 34px;
  align-content: start;
}

.sbl-04__doc section {
  scroll-margin-top: 24px;
}

.sbl-04__doc h1 {
  font: 700 26px/1.2 system-ui,sans-serif;
}

.sbl-04__doc h2 {
  font: 650 20px/1.25 system-ui,sans-serif;
}

.sbl-04__doc h3 {
  font: 600 16px/1.3 system-ui,sans-serif;
}

.sbl-04__doc p {
  margin-top: 8px;
  color: var(--mut);
  font-size: 14px;
  line-height: 1.65;
  max-width: 58ch;
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .sbl-04__doc {
    scroll-behavior: auto;
  }

  .sbl-04__tree a {
    transition: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — indentation derives from one --depth variable per nesting level; the active link lights via .sbl-04:has(#id:target) a[href="#id"]. */
```

How this works

Indentation never gets hard-coded per level. Each <ul> sets --depth one higher than its parent (.sbl-04__tree ul{--depth:1}, .sbl-04__tree ul ul{--depth:2}) and every link derives its padding once: padding-inline-start: calc(12px + var(--depth) * 16px). Add a fourth level and the math already works. The connecting guide rails are border-inline-start on the nested lists, positioned by the same variable, which produces the classic 'tree' look for free.

Active-state sync uses :target: when the reader clicks a sidebar anchor, the URL fragment changes, the matching <section id> becomes :target, and the rule .sbl-04:has(#intro:target) a[href="#intro"] lights the corresponding link. It's a declarative scroll-spy for click-navigation — each doc section pairs with one selector line. scroll-behavior:smooth on the content pane and scroll-margin-top on the headings finish the feel; :focus-within on the tree groups keeps keyboard navigation visibly located.

Make it yours

  • Deeper trees: add ul ul ul{--depth:3} — the padding calc and rails need zero new rules.
  • Swap :target for a server-rendered aria-current="page" on multi-page docs; the highlight selector becomes one attribute rule.
  • The 240px rail width suits 2-level trees; go 280px for API references with long method names.
  • Collapse level-2 groups with <details> (demo 03's pattern) for very large trees — the --depth indentation composes with it.

Gotchas — read before shipping

  • One :target per page — if the demo section IDs collide with other page anchors, prefix them (as done here with sbl04-).
  • scroll-behavior:smooth must live on the element that actually scrolls (the content pane here), not the sidebar.
  • Remember scroll-margin-top on target headings if your real site has a fixed header, or anchors land hidden underneath it.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 105+.

:has() gates the scroll-sync highlight; the depth-variable indentation works in every engine since custom properties (2016).

Techniques used in this demo

Search CodeFronts

Loading…