You Might Also Like
Sticky Sidebar — position:sticky Sidebar HTML CSS JS
Sticky Sidebar · Layouts · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Sticky Sidebar — A position:sticky Aside with Scroll-Spy Table of Contents

A sticky sidebar — one that scrolls with the page until it reaches the top, then stays pinned while the main content keeps moving — is the standard layout for documentation, long articles, and product pages. This snippet builds it with modern position: sticky (no scroll-listener hacks) and adds a table of contents that highlights the section you're currently reading, in plain HTML, CSS, and vanilla JavaScript.
Sticky with one CSS property
The sidebar column (.ss-aside) is set to position: sticky; top: 20px. That's the entire mechanism: the column scrolls normally until its top hits 20px from the viewport top, then it sticks there while the rest of the page scrolls past — and unsticks naturally when its container scrolls away. position: sticky replaced the old approach of listening to scroll and toggling position: fixed with manual offset math; it's smoother, jank-free, and a fraction of the code. The key requirement is that the sticky element's parent be tall enough to scroll within, which the grid layout provides.
Grid layout with top alignment
The page is a two-column grid (1fr 250px) with align-items: start, which is essential: without it, grid would stretch the sidebar column to the full height of the main content, and a sticky child can't stick inside a parent that's exactly as tall as the scroll area. Starting the items at the top lets the sidebar column be only as tall as its content, giving the sticky wrapper room to travel.
Scroll-spy with IntersectionObserver
The table of contents highlights the active section using an IntersectionObserver rather than scroll-position math. Each section is observed with a rootMargin that defines a "reading band" near the top of the viewport (-20% 0px -70% 0px); whichever section enters that band becomes active, and its TOC link lights up. Using IntersectionObserver is both more efficient (the browser does the work, off the main thread) and more accurate than computing offsets on every scroll event — the modern way to build scroll-spy.
Smooth in-page navigation
Clicking a TOC link smooth-scrolls to its section with scrollIntoView({ behavior: 'smooth' }), and scroll-margin-top on each section ensures the heading lands below the sticky offset rather than flush against the top. Together with the active-link highlight, the TOC both navigates and orients.
Responsive collapse
On narrow screens the grid collapses to one column, the sidebar becomes static (un-sticky), and the secondary CTA card hides — because a sticky sidebar makes no sense in a single-column phone layout. This keeps the reading experience clean on mobile while delivering the full sticky-and-spy behaviour on wider screens. It's a complete, drop-in reference for the sticky-sidebar pattern behind every docs site.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to puzzle out why the sidebar sometimes refuses to stick by trial and error. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why align-items: start on the grid is required for position: sticky to have room to travel, and how the rootMargin string of -20% 0px -70% 0px on the IntersectionObserver defines the reading band that decides which TOC link lights up. The same assistant can help optimize it — for instance whether observing many small sections is cheaper than one observer per section, or whether the smooth-scroll click handler should account for the sticky top offset more precisely. It's also useful for extending the pattern: ask it to add a progress indicator that fills each TOC link as its section is read, support nested sub-headings in the TOC, or make the CTA card sticky-within-sticky so it stacks below the TOC once both are visible. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a two-column content layout with a sticky sidebar and scroll-spy table of contents in plain HTML, CSS, and JavaScript using only position: sticky and IntersectionObserver — no scroll-event listener, no fixed-position math.
Requirements:
- A CSS grid layout with a wide main content column and a narrower sidebar column, using align-items: start on the grid container so the sidebar column is only as tall as its own content rather than stretching to match the main column's height.
- The sidebar itself must use position: sticky with a top offset in pixels, relying entirely on native sticky behavior — no JavaScript toggling a fixed class based on scroll position.
- The main content must be split into multiple named sections (each with an id matching a table-of-contents link's href), and each section needs a scroll-margin-top so that scrolling to it does not tuck its heading under the sticky sidebar's top offset.
- Build a table of contents list of anchor links, one per section, and create a single IntersectionObserver (not one per link) that watches all sections with a rootMargin defining a thin horizontal reading band near the top of the viewport, so whichever section's heading currently crosses that band gets its corresponding TOC link marked active.
- Clicking a TOC link must smooth-scroll to its section using scrollIntoView with behavior smooth, preventing the default instant jump.
- On narrow viewports, collapse the grid to a single column and switch the sidebar back to static positioning, hiding any secondary sidebar content that only makes sense in the two-column desktop layout.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSAn article renders with a sidebar containing a table of contents and a CTA card.
- 2Scroll the pageThe sidebar follows until it reaches the top, then stays pinned while the article scrolls.
- 3Watch the active linkThe TOC highlights whichever section is currently in your reading band.
- 4Click a TOC linkIt smooth-scrolls to that section, landing the heading below the sticky offset.
- 5Resize narrowOn phones the layout collapses to one column and the sidebar becomes static.
- 6Adapt itChange the sticky top offset, column widths, or put your own content in the sidebar.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
position:sticky keeps the element in normal flow until it reaches a scroll threshold, then pins it — and automatically releases it when its container scrolls away. position:fixed requires a scroll listener to toggle it on/off and manual math to place it, which is jankier and far more code. Sticky is hardware-accelerated, handles the start and end of the sticky range for you, and needs just two CSS lines.
By default grid stretches items to fill the row's height. If the sidebar column stretches to match the tall main column, it becomes exactly as tall as the scroll area — and a sticky child has nowhere to travel, so it never appears to stick. align-items: start lets the sidebar column be only as tall as its content, leaving room for the sticky wrapper to move and then pin.
An IntersectionObserver watches each section with a rootMargin of "-20% 0px -70% 0px", which defines a thin band near the top of the viewport. When a section's top crosses into that band it reports as intersecting, and its matching TOC link gets the active class. This is more efficient and accurate than recomputing offsets on every scroll event, since the browser handles the detection off the main thread.
Because the sidebar sticks 20px from the top, an in-page link that scrolls a heading flush to the viewport top would tuck it under that offset. scroll-margin-top adds a buffer so scrollIntoView (and native anchor jumps) land the heading just below the sticky region, fully visible. It's the clean, CSS-only way to offset anchor targets.
The layout is pure CSS, so it ports as-is. For the scroll-spy, set up the IntersectionObserver in a useEffect (React), onMounted/onUnmounted (Vue), or ngAfterViewInit/ngOnDestroy (Angular), disconnecting it on cleanup, and track the active id in state to drive the link class. The sticky CSS and rootMargin logic are framework-agnostic.