Docs Layout Without Frameworks: Sticky Sidebar, Content, Right TOC with CSS Grid

Was this helpful?
Ops-minded CSS Grid Docs
No framework. No excuses.

Docs Layout Without Frameworks: Sticky Sidebar, Content, Right TOC with CSS Grid

Your docs page looks fine in a screenshot. Then it hits production: a sticky sidebar that doesn’t stick, a TOC that jitters, and a content column that either stretches like taffy or compresses into a ransom note.

This is the field guide for building a three-column documentation layout with CSS Grid—left nav, center content, right-side TOC—without a framework and without the usual “why does sticky hate me” spiral.

Why this layout breaks in production

Three-column docs pages fail for boring reasons, which is why they fail so often. Not because CSS Grid can’t do it—Grid is excellent at it—but because the page around the grid (headers, scroll containers, overflow rules, injected banners, cookie prompts, and “helpful” wrappers) quietly changes the rules sticky positioning relies on.

When an SRE says “works in staging,” they usually mean “works on my laptop with one viewport and no third-party scripts.” Docs pages are worse: marketing pixels, feedback widgets, and syntax highlighters all join the party. Every one of them can introduce a scroll container or force relayout at scroll time. That’s how you get the classic symptoms:

  • Sticky nav that stops sticking halfway down.
  • Right TOC that overlaps the footer or disappears behind it.
  • Content column that overflows horizontally because of long code lines.
  • Janky scroll when a script recomputes TOC active state too often.

The right approach is to treat layout like production infrastructure: clear ownership, few moving parts, measurable behavior, and a known failure model.

The target: three columns that behave

We want a layout with:

  • Left sidebar (site navigation), sticky under a top header, independently scrollable when long.
  • Main content with a comfortable line length and resilient code blocks.
  • Right TOC (in-page headings), sticky, scrollable, doesn’t steal focus, and doesn’t require a framework.

And we want it to degrade gracefully: on smaller viewports, collapse into a single column without turning into a maze of nested scrollbars.

Opinionated stance: avoid a “full height app shell” with height: 100vh + inner scroll for docs pages. It breaks sticky and makes anchors weird.

CSS Grid core: the one template that works

There are lots of ways to do this. Most of them are fragile. The reliable pattern is: let the body (or document) own scrolling, use Grid for columns, and use position: sticky for the two side panels. Then clamp widths with fixed columns for sidebars and a flexible center column with minmax(0, 1fr).

That minmax(0, 1fr) is not decoration. Without the 0, the center column can refuse to shrink due to intrinsic sizing, and you get overflow. This is the “why does my grid ignore my width” genre of problem.

Grid shape: [sidebar] [content] [toc]

Template: grid-template-columns: 270px minmax(0, 1fr) 250px;

Keep sidebars as normal elements in the grid flow; don’t absolutely position them. Absolute positioning feels clever until you try to print, handle dynamic header height, or support safe areas on mobile.

Reference HTML structure

Minimal, boring, stable:

cr0x@server:~$ cat layout.html
<header>...sticky top bar...</header>
<div class="layout">
  <nav>...left nav...</nav>
  <main>...content...</main>
  <aside>...right toc...</aside>
</div>

The “secret” is that nothing here creates a new scrolling context. That’s how sticky survives contact with the enemy.

Sticky rules: what makes it stick (and what kills it)

position: sticky is simple until it isn’t. Sticky works relative to the nearest scrolling ancestor. If an ancestor has overflow set to anything that creates a scroll container (auto, scroll, hidden in practice), sticky becomes relative to that container instead of the viewport. Sometimes that’s what you want. Most of the time in docs, it’s not.

Here’s the stable configuration:

  • Document scroll (no “inner app scroll”).
  • Sticky sidebars with top: headerHeight + gap.
  • Sidebars get max-height and overflow: auto so only they scroll when too long.

And here are the classic sticky killers:

  • An ancestor with overflow set: a wrapper div with overflow: hidden for a shadow clip, a modal manager, or “fixing” horizontal overflow.
  • Transforms on an ancestor: transform and some filter/will-change patterns can affect containing blocks and scrolling behavior in ways that surprise you.
  • Using height: 100vh with inner scroll: sticky becomes relative to the inner scroll container, and anchor links don’t land where you expect.
Sticky is like a storage array controller: it’s fine until someone adds “just one more layer” of abstraction and nobody remembers why the latency doubled.

Header offsets: don’t guess

If you have a sticky header, anchor jumps will land under it. Set scroll-margin-top on headings and call it done. Don’t hack it with empty offset divs.

Example:

  • h2, h3 { scroll-margin-top: calc(var(--header-h) + 12px); }

Scroll containers: the silent sticky assassin

Most sticky bugs are not sticky bugs. They’re scroll-container bugs. A scroll container is created when an element clips overflow and has a scrollable overflow region. In real codebases, scroll containers appear because someone wanted:

  • to prevent horizontal scrolling from code blocks (overflow-x: hidden on a wrapper)
  • to apply a blur or shadow and clip it
  • a “full height shell” so the footer doesn’t move
  • a virtualization wrapper for search results or a feedback widget

Your job is to identify the nearest scrolling ancestor of the sticky element. In Chrome DevTools you can eyeball it, but in production you want repeatable checks. You can even add a debug CSS mode that outlines elements with overflow properties. Make it a build-time toggle.

Nested scrolling: pick exactly one winner

Docs pages should have exactly one primary scroll: the document. Sidebars can be secondary scroll regions when needed, but the page should not depend on an inner scroll region for basic navigation.

Nested scrolling breaks:

  • browser back/forward scroll restoration
  • anchor links
  • keyboard PageDown behavior
  • some screen reader expectations

One short joke #1: Nested scroll containers are the UI equivalent of RAID 0: impressively fast at turning small mistakes into big consequences.

Readable content width and typography constraints

The center column is where your readers live. A wide screen doesn’t mean you should use it. Keep content width in characters, not pixels. It’s a docs site, not a billboard.

Rules that work:

  • Set max-width on the content flow, e.g., 74ch to 80ch.
  • Allow code blocks to scroll horizontally inside themselves. Don’t “fix” overflow by hiding it on outer wrappers.
  • Use min-width: 0 (or Grid’s minmax(0, 1fr)) so the center column can shrink.

Also: code blocks are the storage workload of docs. They’re spiky, unpredictable, and full of pathological cases like unbroken strings. Treat them like a performance test: constrain them, isolate them, and measure them.

Right-side TOC: markup, offsets, and active states

A right-side TOC is useful when it’s quiet. It should not become a second navigation system that fights your main nav. Keep it minimal:

  • Include only H2/H3. Skip H4 unless you’re writing standards documents.
  • Truncate long headings visually but keep full text for screen readers via aria-label if needed.
  • Don’t auto-expand a tree as you scroll unless you can do it without layout thrash.

Anchor targets: stable IDs, stable headings

Don’t generate IDs at runtime on the client if you can avoid it. It breaks inbound links and makes diffs noisy. Generate IDs at build time (static site generator, markdown processor, or even a tiny script in your pipeline). If you truly have no build step, you can still use deterministic rules, but then your headings must be stable.

Active section highlighting: IntersectionObserver, not scroll math

Old-school TOC scripts used scroll listeners and manual bounding rectangle checks. It works until it doesn’t. The correct primitive is IntersectionObserver, which is designed for “is this heading in view” questions.

That said: you can skip active highlighting entirely. Many teams ship it and then spend months tuning it. If you have limited time, spend it on sticky and typography. Readers forgive a passive TOC. They do not forgive a TOC that causes scroll jank.

Responsive behavior without a framework

On narrow viewports, a three-column layout becomes self-parody. Collapse to one column. You can either:

  • stack nav, content, toc (in that order), or
  • hide the right TOC and provide an in-content mini-TOC near the top

The second option is often better: fewer scroll regions, less noise, fewer mis-taps.

Use one breakpoint around where the content column gets cramped—typically near 1024px depending on sidebar widths. Past that point, treat the view as “mobile” and simplify. Don’t do “two columns with a floating TOC” unless you like debugging edge cases with iOS Safari.

Accessibility: skip links, focus, reduced motion

Docs layouts are navigation-heavy. If you build a fancy sticky shell and forget keyboard users, you’ve shipped a maze with the lights off.

Skip links and landmark roles

Provide a “Skip to content” link as the first focusable element. Use semantic elements (<nav>, <main>, <aside>) and label them with aria-label where helpful.

Focus behavior and scroll

When a user tabs through a sticky sidebar, focus outlines must remain visible and not be clipped by overflow. If your nav is scrollable (overflow: auto), ensure focused items scroll into view. Browsers generally handle this, but it can fail with custom focus management or if you’re doing weird transforms.

Reduced motion

If you add smooth scrolling, respect prefers-reduced-motion. Also consider that smooth scrolling can feel “laggy” on low-end devices. Reliability beats vibes.

Performance and stability: layout thrash is an outage too

Docs pages aren’t supposed to melt laptops. Yet I’ve seen them do it, mostly due to:

  • scroll handlers doing heavy work
  • TOC scripts repeatedly measuring layout (getBoundingClientRect) per frame
  • syntax highlighting rerunning on route changes in SPAs
  • web fonts causing late reflow that shifts anchors

Two hard rules:

  • Don’t run layout reads and writes in the same frame in response to scroll. If you must, batch them.
  • Prefer browser primitives (CSS sticky, IntersectionObserver) over custom polling.

“Hope is not a strategy.” — Gene Kranz

In ops terms: treat scroll performance as a budget. If your docs layout causes long tasks or repeated layout recalcs, it will show up in real user monitoring as “mysterious slowness,” which is the worst kind because it’s hard to reproduce.

Practical tasks with commands (and how to decide)

These tasks assume you have a local environment, a build artifact (even if it’s hand-written HTML), and at least a static server. The commands are the kind you actually run when you’re debugging layout behavior across environments. Each task includes: the command, what the output means, and the decision you make.

Task 1: Serve the site locally with correct caching disabled

cr0x@server:~$ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...

What it means: You’re serving static files without service worker tricks. Good baseline.

Decision: Reproduce the sticky bug here first. If it only happens behind your real CDN/proxy, you’re looking at injected markup/scripts or different headers/CSP.

Task 2: Verify compression and content type (TOC scripts break on wrong MIME)

cr0x@server:~$ curl -I http://127.0.0.1:8080/index.html
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.11.2
Content-type: text/html
Content-Length: 24891
Last-Modified: Sat, 28 Dec 2025 10:11:41 GMT

What it means: Correct MIME and a stable Last-Modified. Some browsers behave oddly if CSS or JS is served as text/plain.

Decision: If MIME is wrong in production, fix server config before debugging CSS. Otherwise you’re debugging ghosts.

Task 3: Confirm the grid columns computed as expected

cr0x@server:~$ node -e "console.log('Use DevTools: Elements → Computed → grid-template-columns')"
Use DevTools: Elements → Computed → grid-template-columns

What it means: There’s no CLI that beats DevTools for computed grid tracks. You’re checking for unintended overrides.

Decision: If you see something like 270px 1000px 250px when you expected the middle to flex, find the rule that removed minmax(0, 1fr) or introduced min-content behavior.

Task 4: Search for overflow rules that create scroll containers

cr0x@server:~$ rg -n "overflow\s*:\s*(auto|scroll|hidden)" ./styles
styles/app.css:41:  overflow: hidden;
styles/layout.css:112: overflow: auto;

What it means: You found potential sticky-killers. overflow: hidden on an ancestor is a prime suspect.

Decision: For each hit, check whether it wraps your sticky elements. If yes, remove it or move it to a narrower scope (e.g., only the code block, not the entire layout wrapper).

Task 5: Identify unexpected wrappers from build tooling

cr0x@server:~$ rg -n "layout|wrapper|container|shell" ./dist/index.html
52:<div class="app-shell">
53:  <div class="page-wrapper">

What it means: Your “simple page” is sitting inside an app shell. That shell often owns scrolling.

Decision: If .app-shell uses height: 100vh + overflow: auto, you either refactor the shell or accept sticky will stick inside that container and adjust expectations (anchors, scroll restoration, etc.). I recommend refactoring for docs.

Task 6: Check whether any parent uses transforms (can break fixed/sticky assumptions)

cr0x@server:~$ rg -n "transform\s*:" ./styles
styles/marketing.css:88: transform: translateZ(0);
styles/marketing.css:212: transform: scale(1.02);

What it means: Someone applied transform hacks for “smoothness.” Often cargo cult.

Decision: If those transformed elements wrap the layout, remove the transform or move it to a child. Then re-test sticky. Don’t keep GPU hacks unless you can justify them with measured results.

Task 7: Confirm the header height is what your sticky offset expects

cr0x@server:~$ rg -n "--header-h" ./styles
styles/app.css:17: --header-h: 56px;

What it means: You’re using a CSS variable for header height. Good. Now verify the header actually is 56px across breakpoints.

Decision: If the header wraps on smaller screens and becomes taller, you must update --header-h responsively or avoid depending on a fixed pixel value (e.g., compute using layout, or use top with a smaller value and give headings scroll-margin-top that overcompensates).

Task 8: Catch horizontal overflow introduced by code blocks

cr0x@server:~$ rg -n "pre\s*\{|code\s*\{|white-space|word-break|overflow-x" ./styles
styles/code.css:9: pre { overflow-x: auto; }
styles/code.css:10: pre { white-space: pre; }

What it means: Your code blocks will scroll internally, which is correct. If you instead see overflow-x: hidden on outer containers, that’s a smell.

Decision: Keep overflow control at the code block level. If the layout wrapper hides overflow, sticky may break and focus outlines get clipped.

Task 9: Validate that anchors exist and are unique

cr0x@server:~$ rg -n "id=\"" ./dist/index.html | head
118:<h2 id="grid-core">CSS Grid core: the one template that works</h2>
156:<h2 id="sticky-rules">Sticky rules: what makes it stick (and what kills it)</h2>

What it means: IDs exist. Now confirm uniqueness.

Decision: If IDs repeat, browsers will jump to the first match, your TOC will appear “wrong,” and debugging will be annoying. Fix at generation time.

Task 10: Ensure CSP isn’t blocking your TOC script (common in corporate setups)

cr0x@server:~$ curl -I https://docs.example.internal/ | rg -i "content-security-policy"
Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'self'

What it means: Inline scripts might be blocked. If your TOC relies on inline JS, it won’t run.

Decision: Move scripts to static files served from 'self', or add nonces/hashes. For docs, prefer no-JS TOC and optional enhancement.

Task 11: Detect client-side long tasks that correlate with scroll

cr0x@server:~$ google-chrome --enable-logging=stderr --v=1 2>&1 | head
[1228/101521.402:VERBOSE1:chrome_main_delegate.cc(764)] basic startup complete
[1228/101521.409:VERBOSE1:startup_browser_creator.cc(157)] Launched chrome

What it means: This is just startup logging; the real work is in DevTools Performance recordings. Still, launching with logs helps when GPU/compositor issues are suspected.

Decision: If performance recordings show repeated layout recalculation during scroll, audit TOC scripts and any scroll listeners. Replace with IntersectionObserver or throttle to sane intervals.

Task 12: Confirm no service worker is caching broken CSS/JS

cr0x@server:~$ rg -n "serviceWorker|navigator\.serviceWorker" ./dist
dist/app.js:14:navigator.serviceWorker.register("/sw.js")

What it means: A service worker exists. It can pin old CSS causing “it’s fixed but not fixed” behavior.

Decision: For docs, consider avoiding service workers unless you truly need offline. If you keep it, implement cache busting and a clear update flow.

Task 13: Check HTTP caching headers for CSS (stale CSS causes phantom regressions)

cr0x@server:~$ curl -I https://docs.example.internal/assets/app.css | rg -i "cache-control|etag|last-modified"
Cache-Control: public, max-age=31536000, immutable
ETag: "a8b3c-5f1c2d9b"

What it means: Immutable caching is fine only if filenames are content-hashed.

Decision: If the filename is static (e.g., app.css), remove immutable caching or add hashing. Otherwise layout fixes won’t land for users.

Task 14: Validate that your HTML has a single main landmark

cr0x@server:~$ tidy -errors -q ./dist/index.html | head
line 1 column 1 - Warning: missing 
 declaration
line 54 column 5 - Warning: 
should not appear as a child of

What it means: HTML structure issues can confuse assistive tech and sometimes your CSS selectors. (Also: add the doctype.)

Decision: Fix structure warnings that affect landmarks and headings. Don’t chase cosmetic warnings unless they’re causing real problems.

Task 15: Audit heading hierarchy (TOC quality depends on it)

cr0x@server:~$ rg -n "

What it means: Headings exist and appear ordered. If you see h4 before h2, your TOC generator will do silly things.

Decision: Enforce heading order in authoring guidelines and CI checks. Content discipline pays dividends.

Fast diagnosis playbook

When the layout is broken and someone is asking “is this a CSS bug,” you don’t have time for vibes. You need a short playbook that finds the bottleneck fast.

First: confirm who owns scrolling

  • Check: does the page scroll on body/html, or is there an inner container with its own scrollbar?
  • Why: sticky behaves relative to the nearest scroll container.
  • Action: if there’s an inner scroll container, decide whether to remove it (preferred for docs) or adjust sticky and anchor offsets accordingly.

Second: find the nearest overflow/transform ancestor

  • Check: inspect sidebar and TOC; walk up the DOM looking for overflow, transform, filter, contain.
  • Why: any of these can change sticky’s containing context or clip it.
  • Action: remove or narrow the scope; don’t blanket-apply overflow fixes to layout wrappers.

Third: verify grid track sizing and min-width behavior

  • Check: ensure the center column is minmax(0, 1fr) and that the content or its children don’t have min-width forcing overflow.
  • Why: intrinsic sizing from code blocks and long strings will destroy your layout.
  • Action: set min-width: 0 on grid children as needed, and isolate overflow to code blocks.

Fourth: measure scroll jank

  • Check: record a Performance trace while scrolling.
  • Why: TOC scripts are frequent offenders.
  • Action: replace scroll handlers with IntersectionObserver, or kill active highlighting if you can’t make it cheap.

Common mistakes (symptom → root cause → fix)

Sticky sidebar doesn’t stick at all

Symptom: Sidebar scrolls away like a normal element.

Root cause: Missing top on the sticky element, or sticky is applied to the wrong node (e.g., inner list, not the panel).

Fix: Apply position: sticky and top: ... to the sidebar container. Ensure it’s not inside a transformed ancestor.

Sticky works until a certain point, then stops

Symptom: Sidebar sticks initially, then “lets go.”

Root cause: The sticky element is constrained by the height of its containing block (often because the grid row or wrapper ends earlier than you think).

Fix: Keep the sticky element in the same scrolling context as the main document. Avoid wrappers with overflow that clip. Ensure the grid container spans the full content height (usually happens naturally if you don’t force heights).

Right TOC overlaps footer or page end

Symptom: TOC panel sits on top of footer content.

Root cause: Using position: fixed instead of sticky, or a layout wrapper that doesn’t reserve space for the TOC.

Fix: Use sticky inside the grid cell so it naturally ends when content ends. Avoid fixed positioning unless you truly want a floating panel.

Content column causes horizontal scroll on the whole page

Symptom: You can scroll the entire page sideways. Everyone hates it.

Root cause: Long code lines or unbroken strings, plus a grid track that won’t shrink because the middle column is 1fr without minmax(0, ...).

Fix: Use minmax(0, 1fr). Put overflow-x: auto on pre. Do not put overflow-x: hidden on outer wrappers as a “fix.”

Anchors jump under the header

Symptom: Clicking TOC lands you with the heading hidden behind the sticky header.

Root cause: No scroll-margin-top on headings.

Fix: Add scroll-margin-top with header height + gap. Keep it in CSS, not in JavaScript.

Active TOC highlight lags behind or flickers

Symptom: Highlight jumps between headings or updates late.

Root cause: Scroll listener doing too much work, or thresholds not tuned; also common with variable header heights.

Fix: Use IntersectionObserver and choose a sensible root margin (e.g., top offset matching header). Or remove active highlighting.

Sidebar scroll steals wheel events from the page

Symptom: User scrolls over the sidebar and gets “stuck” scrolling the nav.

Root cause: Sidebars are scrollable and intercept wheel/touch events.

Fix: Accept it as normal behavior, but keep sidebars short where possible; consider collapsible sections. Avoid aggressive scroll chaining hacks unless you know what you’re doing.

Print output is unreadable or missing content

Symptom: Printed page includes nav/TOC noise or clips content.

Root cause: Sticky panels and backgrounds not handled for print.

Fix: Add a @media print rule to hide nav/aside and remove heavy backgrounds and shadows.

Checklists / step-by-step plan

Step-by-step: build the layout (the sane order)

  1. Start with semantic HTML: header, nav, main, aside. No wrappers unless they earn their keep.
  2. Make the grid: fixed left and right columns; center with minmax(0, 1fr).
  3. Set content readability constraints: cap line length with max-width on the content flow; make code blocks scroll internally.
  4. Add sticky behavior: sticky left and right panels with top based on header height; use max-height + overflow: auto for long lists.
  5. Fix anchor offsets: scroll-margin-top on headings.
  6. Responsive collapse: single column under one breakpoint; consider hiding the right TOC.
  7. Accessibility pass: skip link, focus styling, landmark labels, keyboard navigation.
  8. Performance pass: kill scroll handlers; if you must highlight active headings, use IntersectionObserver.
  9. Production hardening: check for injected wrappers/scripts; lock down overflow and transforms around layout.

Operational checklist: what to review in PRs

  • No new overflow: hidden on high-level wrappers without a justification and screenshots across breakpoints.
  • No new height: 100vh shells for docs pages (unless you accept the consequences and document them).
  • Grid template keeps minmax(0, 1fr) (or equivalent min-width: 0 on content).
  • Headings have stable IDs; no duplicate IDs.
  • Print styles exist and hide navigation clutter.
  • Any TOC JS is optional and doesn’t break without JS.

Three corporate mini-stories from the trenches

Mini-story 1: An incident caused by a wrong assumption

A team I worked with shipped a docs redesign inside a “unified app shell.” The shell had a fixed header, a left sidebar, and an inner content region with height: 100vh and overflow: auto. The assumption was straightforward: “this is what modern apps do, and sticky will work inside it.”

It did work. In Chrome. On desktops. In the happy path where the header height never changed.

Then a corporate banner was injected at the top for a compliance notice. Header height increased. Anchor links started landing under the header. The right-side TOC (which used a scroll listener) computed the wrong offsets because it assumed the scroll root was the window, not the inner container. Users clicked headings and thought the content was missing. Support tickets started coming in with screenshots of “blank sections” that were, in reality, hidden above the fold.

The incident wasn’t a crash. It was worse: a slow bleed of trust. Engineers stopped relying on the docs. They asked colleagues instead, which is an expensive way to do information retrieval.

The fix was boring: remove the inner scroll container, let the document scroll, set scroll-margin-top, and make the banner part of normal flow so the layout adapted naturally. The assumption that “app shell patterns are universal” was the root cause. Docs are content-first, not chrome-first.

Mini-story 2: An optimization that backfired

Another org wanted “buttery smooth scrolling” on long docs pages. Someone introduced a pattern: apply transform: translateZ(0) to the main wrapper to force compositor acceleration, plus will-change: transform on a few panels. The change came with a nice demo video.

In production, the right TOC started behaving inconsistently. On some machines, sticky positioning stopped working when the wrapper became a transformed containing block. On others, it worked but text rendering changed slightly (subpixel differences) and headings shifted just enough that IntersectionObserver thresholds started flickering. The active heading highlight danced between two sections while the user scrolled slowly. Nothing says “quality engineering” like a navigation widget having an identity crisis.

Performance also got worse on lower-end devices. The browser kept extra layers alive due to will-change, increasing memory pressure and occasionally triggering garbage collection pauses. Users described the page as “stuttery,” which is the UI way of saying “your optimization is the problem.”

The rollback was immediate. The long-term fix was disciplined: keep transforms off layout wrappers, measure actual scroll performance with a trace, and only apply will-change to elements that truly animate. Most docs pages don’t need animation; they need predictability.

Mini-story 3: A boring but correct practice that saved the day

One team had a habit that felt tedious: every layout change came with a small “layout invariants” test page in the repo. It wasn’t fancy. Just a single HTML file that included pathological content: very long code lines, deeply nested headings, an oversized nav list, a fake top banner, and a footer.

Before shipping, they’d open that page in a few browsers, resize the viewport, and click through anchors. It took ten minutes. People complained, gently, as engineers do.

Then a third-party feedback widget update landed and wrapped the entire page in a container with overflow: hidden to manage its own animations. Sticky would have broken. But because the team had the invariants page and checked it in a staging environment that matched production integrations, the regression was caught before release.

The fix was not heroic: scope the widget container so it didn’t wrap the docs layout, and remove the overflow rule from the high-level wrapper. The boring practice—keeping a pathological test page and using it—saved days of support churn and avoided shipping a broken navigation experience to every engineer in the company.

One short joke #2: The only thing stickier than position: sticky is a bug report titled “docs navigation broken” filed five minutes before a release freeze.

Facts and historical context

  • CSS Grid became broadly usable in modern browsers around 2017, replacing a decade of float hacks and fragile column systems for many layouts.
  • Before Grid, “holy grail” three-column layouts were typically built with floats or table-like display modes, often requiring source-order tricks.
  • position: sticky started as a long-requested capability because “fixed” positioning is too blunt for in-flow UI like sidebars.
  • Sticky behavior depends on scroll containers, and scroll containers became more common with SPA shells and component libraries that default to internal scrolling.
  • The minmax(0, 1fr) pattern exists because of intrinsic sizing rules; without it, content like long code lines can force grid tracks wider than the viewport.
  • Character units (ch) are a practical typography tool for docs because they scale with the font and better represent line length than pixels.
  • IntersectionObserver was introduced to reduce scroll-handler abuse, giving the platform a more efficient way to observe visibility changes.
  • Docs sites historically leaned on server-rendered HTML because linking, printing, and accessibility are non-negotiable; heavy client routing came later and often regressed these basics.

FAQ

1) Should I use CSS Grid or Flexbox for this layout?

Use Grid. This is literally what Grid is for: two fixed tracks and one flexible track with clean gaps. Flexbox can do it, but it’s more fragile once you add independent scrolling panels and responsive collapse.

2) Why do you insist on minmax(0, 1fr)?

Because the default minimum size of grid items can be their intrinsic content width. Long code lines will force overflow. minmax(0, 1fr) explicitly allows the track to shrink.

3) Is it okay to make the entire page a fixed-height shell with internal scrolling?

You can, but you’re buying problems: anchor links, scroll restoration, sticky offsets, and mobile browser quirks. For docs, I recommend letting the document scroll unless you have a very specific requirement.

4) My sticky element works until I add overflow: hidden somewhere. Why?

Because you created (or changed) the element that defines the sticky containing context. Sticky positioning is relative to the nearest scrolling ancestor. Overflow rules often create that ancestor or alter clipping behavior.

5) Should the right-side TOC be generated with JavaScript?

If you have a build step, generate it at build time. If you don’t, keep a simple static TOC or use progressive enhancement. Don’t require JS for basic navigation on a docs site unless you enjoy explaining that decision later.

6) How do I prevent the TOC from being too tall?

Give it max-height: calc(100dvh - topOffset - bottomGap) and overflow: auto. Let it scroll internally. If it’s still massive, you have too many headings; that’s a content design issue, not a CSS issue.

7) How do I avoid the TOC overlapping content on smaller screens?

Collapse to one column at a breakpoint and either hide the right TOC or move an in-content TOC to the top of the article. Trying to keep three columns on mobile is how you get accidental horizontal scroll and rage clicks.

8) Does position: sticky hurt performance?

Sticky itself is generally fine. The performance killers are scroll listeners, layout thrash, and heavy paint effects (shadows, filters) on large sticky panels. Measure with a performance trace; don’t guess.

9) What about safe areas and notches on mobile?

If you have a sticky header, consider using environment variables like safe-area insets in your padding. But keep it simple: docs pages shouldn’t have intricate fixed chrome on mobile.

10) Can I support printing cleanly with this layout?

Yes. Hide nav and TOC in @media print, remove backgrounds and shadows, keep links underlined. Print is still used in audits, reviews, and incident runbooks—annoyingly often.

Next steps that actually help

If you want this layout to survive production, do the boring stuff:

  • Lock down the scroll model: document scroll, not an inner shell.
  • Make Grid resilient: fixed sidebars, minmax(0, 1fr) in the center, and avoid intrinsic sizing surprises.
  • Make sticky predictable: no overflow/transform wrappers around the sticky elements.
  • Fix anchors properly: scroll-margin-top on headings, not hack divs.
  • Keep TOC enhancement optional: if active highlighting causes jank, remove it. Nobody got paged because a TOC didn’t glow.

Then run the same checks you’d run for a service: reproduce locally, verify in a production-like environment with injected scripts, and keep a pathological test page. Docs are infrastructure. Treat them like it.

← Previous
Proxmox “Unable to activate storage”: Diagnose LVM, NFS, and CIFS the right way
Next →
ZFS Boot Environments: The Upgrade Safety Net Linux Users Ignore

Leave a comment