Sticky Sidebar — position:sticky Sidebar HTML CSS JS

Sticky Sidebar · Layouts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

position:sticky sidebar
One CSS property pins the sidebar — no scroll listeners or fixed-position math.
Grid with align-items:start
Top-aligned columns give the sticky element room to travel within its parent.
IntersectionObserver scroll-spy
The active section is detected by a reading-band rootMargin, off the main thread.
Active TOC highlight
The link for the section in view lights up as you scroll.
Smooth in-page links
Clicking a TOC link smooth-scrolls with scrollIntoView.
scroll-margin-top offset
Sections land below the sticky offset, not flush against the top.
Responsive collapse
Single column on phones with the sidebar un-stuck and the CTA hidden.
No library
Pure HTML/CSS/JS using modern sticky + IntersectionObserver.

About this UI Snippet

Sticky Sidebar — A position:sticky Aside with Scroll-Spy Table of Contents

Screenshot of the Sticky Sidebar snippet rendered live

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:

text
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

  1. 1
    Paste HTML, CSS, and JSAn article renders with a sidebar containing a table of contents and a CTA card.
  2. 2
    Scroll the pageThe sidebar follows until it reaches the top, then stays pinned while the article scrolls.
  3. 3
    Watch the active linkThe TOC highlights whichever section is currently in your reading band.
  4. 4
    Click a TOC linkIt smooth-scrolls to that section, landing the heading below the sticky offset.
  5. 5
    Resize narrowOn phones the layout collapses to one column and the sidebar becomes static.
  6. 6
    Adapt itChange the sticky top offset, column widths, or put your own content in the sidebar.

Real-world uses

Common Use Cases

Documentation sites
Pin a section TOC beside docs content — pair with a table of contents for deeper nesting.
Long-form articles and blogs
Keep a reading progress TOC in view alongside a reading progress bar.
Product and pricing pages
Stick a summary or CTA card next to scrolling feature content.
Checkout and forms
Keep an order summary visible beside a long form, like a checkout form.
Dashboards and reports
Pin filters or a legend while data scrolls.
Learning position:sticky & scroll-spy
A reference for sticky layout and IntersectionObserver — compare with a scroll-spy nav.

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.