DEV3 Technologies home

Docs sidebar with scroll-spy

A sticky table of contents that highlights the section currently in view, driven by IntersectionObserver.

Tailwind CSSNavigationdocssidebarscroll-spytoc

Live preview

Code

<div class="mx-auto flex max-w-5xl gap-10 px-4 py-10">
  <nav aria-label="On this page" class="sticky top-20 hidden h-fit w-56 shrink-0 lg:block">
    <p class="mb-3 text-xs font-semibold uppercase tracking-wider text-slate-500">On this page</p>
    <ul id="toc" class="space-y-1 border-l border-slate-200 text-sm">
      <li><a href="#install" class="-ml-px block border-l-2 border-transparent py-1 pl-4 text-slate-600 hover:text-slate-900">Install</a></li>
      <li><a href="#configure" class="-ml-px block border-l-2 border-transparent py-1 pl-4 text-slate-600 hover:text-slate-900">Configure</a></li>
      <li><a href="#deploy" class="-ml-px block border-l-2 border-transparent py-1 pl-4 text-slate-600 hover:text-slate-900">Deploy</a></li>
    </ul>
  </nav>

  <article class="min-w-0 flex-1 space-y-16">
    <section id="install" class="scroll-mt-24"><h2 class="text-2xl font-bold">Install</h2><p class="mt-3 h-96 text-slate-600">Add the package to your project.</p></section>
    <section id="configure" class="scroll-mt-24"><h2 class="text-2xl font-bold">Configure</h2><p class="mt-3 h-96 text-slate-600">Set the options you need.</p></section>
    <section id="deploy" class="scroll-mt-24"><h2 class="text-2xl font-bold">Deploy</h2><p class="mt-3 h-96 text-slate-600">Ship it to production.</p></section>
  </article>
</div>

How it works

What it solves

Long documentation pages need a map that tells readers where they are. Listening to scroll events and measuring offsets is wasteful and jittery; this version lets the browser tell you which heading crossed a line.

How it works

The sidebar is a sticky nav with a list of in-page links whose href values match section ids. The script builds an IntersectionObserver with a negative top and bottom rootMargin, which shrinks the observed viewport to a thin band near the top. When a heading enters that band, its link receives the active classes (a left border and stronger text) and the previous one loses them. Clicking a link uses native anchor navigation, and scroll-mt-24 on each section keeps the heading clear of a fixed header. The sidebar is hidden below lg where a top-of-page contents list would be better.

Adapting it

Change rootMargin to move the activation line. Generate the link list from your headings if content is dynamic. For nested headings, add a second list level and mark the parent when any child is active.

Pitfalls

  • If two headings sit inside the band at once, the last one wins; keep sections taller than the band.
  • Active classes must be toggled, not replaced, or hover styles vanish.
  • Add scroll-behavior: smooth only with a reduced-motion guard.

How to use it

  1. Put docs-sidebar.html beside your article and give each section an id matching a link.
  2. Add scroll-mt-24 to every section heading.
  3. Load scroll-spy.js with defer.
  4. Change rootMargin if your header height differs.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets