30 CSS Grid Layouts02 / 30

Pure CSSMIT licensed

Sticky Sidebar Grid

A docs-style two-column grid where the table of contents stays pinned while the article scrolls past — the pattern developers fight with for hours because one missing declaration silently kills position: sticky inside grid. This demo names the culprit (align-self: start) and ships the fix.

Published

Live Demo
Try it

The code

<section class="cgl-02" aria-label="Sticky sidebar grid demo">
  <div class="cgl-02__shell">
    <aside class="cgl-02__side">
      <nav class="cgl-02__nav" aria-label="Section navigation">
        <p class="cgl-02__navtitle">On this page</p>
        <a href="#cgl02-a" class="is-active">Getting started</a>
        <a href="#cgl02-b">Why sticky breaks</a>
        <a href="#cgl02-c">The align-self fix</a>
        <a href="#cgl02-d">Overflow traps</a>
      </nav>
    </aside>
    <main class="cgl-02__main">
      <article id="cgl02-a"><h3>Getting started</h3><p>Scroll this panel — the sidebar nav stays pinned while the article moves. Two grid columns, one sticky child, zero JavaScript.</p></article>
      <article id="cgl02-b"><h3>Why sticky breaks</h3><p>Grid stretches items to fill the row's block size. A stretched sidebar is already as tall as its track, so it has no room to travel — sticky computes but never visibly engages.</p></article>
      <article id="cgl02-c"><h3>The align-self fix</h3><p>Setting align-self: start shrinks the sidebar to its content height. Now the track is taller than the item, and sticky has the travel distance it needs.</p></article>
      <article id="cgl02-d"><h3>Overflow traps</h3><p>The second silent killer: any ancestor with overflow: hidden becomes the sticky containing block and clips the effect. Keep the chain clean.</p></article>
    </main>
  </div>
</section>
.cgl-02,
.cgl-02 *,
.cgl-02 *::before,
.cgl-02 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cgl-02 {
  width: 100%;
  min-height: 100vh;
  --bg: oklch(0.18 0.02 265);
  --panel: oklch(0.22 0.025 265);
  --txt: oklch(0.92 0.01 260);
  --dim: oklch(0.68 0.02 260);
  --lime: oklch(0.85 0.19 130);
  font-family: system-ui,sans-serif;
  background: var(--bg);
  color: var(--txt);
  padding: 1.2rem;
}

.cgl-02 .cgl-02__shell {
  display: grid;
  grid-template-columns: minmax(200px,240px) minmax(0,1fr);
  gap: 1.2rem;
  block-size: 340px;
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid oklch(0.32 0.03 265);
  border-radius: 14px;
  padding: 1.2rem;
  background: var(--panel);
}

.cgl-02 .cgl-02__side {
  grid-column: 1;
}

.cgl-02 .cgl-02__nav {
  position: sticky;
  inset-block-start: 0;
  align-self: start;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: oklch(0.26 0.03 265);
  border: 1px solid oklch(0.34 0.03 265);
  border-radius: 10px;
  padding: .9rem;
}

.cgl-02 .cgl-02__navtitle {
  font-size: .68rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--dim);
  margin-block-end: .5rem;
}

.cgl-02 .cgl-02__nav a {
  font-size: .85rem;
  color: var(--dim);
  text-decoration: none;
  padding: .45rem .6rem;
  border-radius: 7px;
  border-inline-start: 2px solid transparent;
}

.cgl-02 .cgl-02__nav a:hover {
  color: var(--txt);
  background: oklch(0.3 0.03 265);
}

.cgl-02 .cgl-02__nav a.is-active {
  color: var(--lime);
  border-inline-start-color: var(--lime);
  background: color-mix(in oklab,var(--lime) 10%,transparent);
}

.cgl-02 .cgl-02__nav a:focus-visible {
  outline: 2px solid var(--lime);
  outline-offset: 2px;
}

.cgl-02 .cgl-02__main {
  display: flex;
  flex-direction: column;
  gap: 1.4rem;
}

.cgl-02 .cgl-02__main article {
  scroll-margin-block-start: 1rem;
}

.cgl-02 h3 {
  font-size: 1.05rem;
  margin-block-end: .4rem;
  color: var(--lime);
}

.cgl-02 p {
  font-size: .88rem;
  line-height: 1.65;
  color: var(--dim);
}

@media (max-width: 600px) {
  .cgl-02 .cgl-02__shell {
    grid-template-columns: 1fr;
  }

  .cgl-02 .cgl-02__nav {
    position: static;
  }
}
/* No JavaScript — align-self: start un-stretches the sidebar so position: sticky can engage inside its grid track. */
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 Grid 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: Sticky Sidebar Grid
Source: https://codefronts.com/layouts/css-grid-layouts/sticky-sidebar-grid/

A docs-style two-column grid where the table of contents stays pinned while the article scrolls past — the pattern developers fight with for hours because one missing declaration silently kills position: sticky inside grid. This demo names the culprit (align-self: start) and ships the fix.
## HTML
```html
<section class="cgl-02" aria-label="Sticky sidebar grid demo">
  <div class="cgl-02__shell">
    <aside class="cgl-02__side">
      <nav class="cgl-02__nav" aria-label="Section navigation">
        <p class="cgl-02__navtitle">On this page</p>
        <a href="#cgl02-a" class="is-active">Getting started</a>
        <a href="#cgl02-b">Why sticky breaks</a>
        <a href="#cgl02-c">The align-self fix</a>
        <a href="#cgl02-d">Overflow traps</a>
      </nav>
    </aside>
    <main class="cgl-02__main">
      <article id="cgl02-a"><h3>Getting started</h3><p>Scroll this panel — the sidebar nav stays pinned while the article moves. Two grid columns, one sticky child, zero JavaScript.</p></article>
      <article id="cgl02-b"><h3>Why sticky breaks</h3><p>Grid stretches items to fill the row's block size. A stretched sidebar is already as tall as its track, so it has no room to travel — sticky computes but never visibly engages.</p></article>
      <article id="cgl02-c"><h3>The align-self fix</h3><p>Setting align-self: start shrinks the sidebar to its content height. Now the track is taller than the item, and sticky has the travel distance it needs.</p></article>
      <article id="cgl02-d"><h3>Overflow traps</h3><p>The second silent killer: any ancestor with overflow: hidden becomes the sticky containing block and clips the effect. Keep the chain clean.</p></article>
    </main>
  </div>
</section>
```
## CSS
```css
.cgl-02,
.cgl-02 *,
.cgl-02 *::before,
.cgl-02 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cgl-02 {
  width: 100%;
  min-height: 100vh;
  --bg: oklch(0.18 0.02 265);
  --panel: oklch(0.22 0.025 265);
  --txt: oklch(0.92 0.01 260);
  --dim: oklch(0.68 0.02 260);
  --lime: oklch(0.85 0.19 130);
  font-family: system-ui,sans-serif;
  background: var(--bg);
  color: var(--txt);
  padding: 1.2rem;
}

.cgl-02 .cgl-02__shell {
  display: grid;
  grid-template-columns: minmax(200px,240px) minmax(0,1fr);
  gap: 1.2rem;
  block-size: 340px;
  overflow-y: auto;
  overscroll-behavior: contain;
  border: 1px solid oklch(0.32 0.03 265);
  border-radius: 14px;
  padding: 1.2rem;
  background: var(--panel);
}

.cgl-02 .cgl-02__side {
  grid-column: 1;
}

.cgl-02 .cgl-02__nav {
  position: sticky;
  inset-block-start: 0;
  align-self: start;
  display: flex;
  flex-direction: column;
  gap: 2px;
  background: oklch(0.26 0.03 265);
  border: 1px solid oklch(0.34 0.03 265);
  border-radius: 10px;
  padding: .9rem;
}

.cgl-02 .cgl-02__navtitle {
  font-size: .68rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--dim);
  margin-block-end: .5rem;
}

.cgl-02 .cgl-02__nav a {
  font-size: .85rem;
  color: var(--dim);
  text-decoration: none;
  padding: .45rem .6rem;
  border-radius: 7px;
  border-inline-start: 2px solid transparent;
}

.cgl-02 .cgl-02__nav a:hover {
  color: var(--txt);
  background: oklch(0.3 0.03 265);
}

.cgl-02 .cgl-02__nav a.is-active {
  color: var(--lime);
  border-inline-start-color: var(--lime);
  background: color-mix(in oklab,var(--lime) 10%,transparent);
}

.cgl-02 .cgl-02__nav a:focus-visible {
  outline: 2px solid var(--lime);
  outline-offset: 2px;
}

.cgl-02 .cgl-02__main {
  display: flex;
  flex-direction: column;
  gap: 1.4rem;
}

.cgl-02 .cgl-02__main article {
  scroll-margin-block-start: 1rem;
}

.cgl-02 h3 {
  font-size: 1.05rem;
  margin-block-end: .4rem;
  color: var(--lime);
}

.cgl-02 p {
  font-size: .88rem;
  line-height: 1.65;
  color: var(--dim);
}

@media (max-width: 600px) {
  .cgl-02 .cgl-02__shell {
    grid-template-columns: 1fr;
  }

  .cgl-02 .cgl-02__nav {
    position: static;
  }
}
```

## JavaScript
```js
/* No JavaScript — align-self: start un-stretches the sidebar so position: sticky can engage inside its grid track. */
```

How this works

The shell is grid-template-columns: minmax(200px, 240px) minmax(0, 1fr) — the sidebar flexes between 200 and 240px, the content column's minmax(0, 1fr) prevents wide children (code blocks, tables) from blowing the track open.

The sidebar nav is position: sticky; inset-block-start: 1rem — and critically align-self: start. Grid stretches items to full track height by default, and a stretched item has nowhere to travel, so sticky never engages. Shrinking it to its content height with align-self: start is the one-line fix behind 90% of "sticky not working in grid" bugs.

Make it yours

  • Move the sidebar right by swapping the column order — sticky behaviour is unaffected.
  • Stack below 720px with one media query that collapses to a single column (included).
  • Adjust the pin offset via inset-block-start to clear a fixed site header.
  • Add max-block-size: calc(100dvh - 2rem) + overflow-y: auto to the nav for very long TOCs.

Gotchas — read before shipping

  • align-self: start (or start-aligned content) is mandatory — a default-stretched grid item can never stick.
  • Any overflow: hidden on an ancestor kills sticky for all descendants; audit the whole chain.
  • Use minmax(0, 1fr) for the content track — plain 1fr means minmax(auto,1fr) and wide code blocks will push the sidebar off-canvas.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

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

Sticky-in-grid works in every engine since 2017 once align-self is set; dvh units in the optional scroll cap need 2022+ evergreens.

Techniques used in this demo

Search CodeFronts

Loading…