If your articles regularly run past 1,500 words, a good sticky table of contents design is one of the cheapest upgrades you can ship. It turns a wall of scrolling into a navigable document, it tells readers what they are about to get, and it keeps the article structure visible at all times instead of disappearing after the first screen.
This guide covers the practical decisions: where to place the component, how to build scroll-spy highlighting that does not misfire, what to do on mobile, how it changes reading behaviour, and the SEO side effects worth caring about. Code snippets are framework free so you can drop them into WordPress, a static site or a custom stack.
What is a sticky table of contents?
A sticky table of contents (TOC) is an in-page navigation list of an article’s headings that stays visible while the user scrolls, usually pinned in a side rail with position: sticky. Each item is an anchor link to a heading ID, and the item matching the section currently in the viewport is visually highlighted (this is called scroll-spy). There’s a good explainer over at smashingmagazine.com.
The difference with a static TOC is simple but decisive: a static TOC is a one-time menu that scrolls away and is forgotten, while a sticky TOC is a persistent wayfinding tool. Nielsen Norman Group makes the same point in its TOC guidance: a sticky table of contents is not lost after the user scrolls down the page, which is exactly why it belongs in a left or right rail on long-form content.

Why a sticky TOC is worth the build time
| Benefit | What actually happens |
|---|---|
| Faster answer-finding | Readers who arrive with a narrow question jump straight to the relevant H2 instead of bouncing back to the SERP. |
| Perceived effort drops | A visible 8 item outline makes a 3,000 word article feel finite. Progress is legible. |
| Higher scroll depth on the sections that matter | Non-linear readers skip filler and land on the section they want, which usually lifts time on the page that counts. |
| Fewer missed sections | E-commerce testing on long product pages found sticky TOCs cut section oversight dramatically compared with non-sticky versions. The same logic applies to long editorial pages. |
| SEO side effects | Clean heading IDs and anchor links give search engines explicit passage structure and can support jump-to-section links in results. |
When you should not use one
- Articles under roughly 1,000 words or with fewer than 4 H2s. The component costs more attention than it saves.
- Narrative or persuasive pieces meant to be read top to bottom, where skipping breaks the argument.
- Layouts with no free horizontal space at your target breakpoint. A cramped rail is worse than no rail.
- Pages that already carry a sticky header, a sticky CTA and a cookie bar. Stacking sticky elements eats the viewport.
Step 1: Choose the placement
Placement is the single biggest decision in sticky table of contents design, and it is mostly dictated by your grid.
| Pattern | Best for | Watch out for |
|---|---|---|
| Left rail | Documentation and knowledge bases where navigation is the primary job | Competes with global nav, can push the article off centre |
| Right rail | Editorial blogs. Keeps the reading column in the optical centre | Conflicts with sticky ads or share widgets |
| Inline collapsible box | Narrow single column templates | Not persistent unless paired with a sticky fallback |
| Sticky top bar with current section | Mobile and tablet | Costs vertical space, needs a compact height (44 to 56px) |
| Floating button opening a sheet | Very long mobile pages and guides | Discoverability. Needs a clear label, not just an icon |
Rule of thumb: right rail on desktop from 1,100px up, collapsible inline block plus a compact sticky bar below that.

Step 2: Design the component itself
Depth and length
- Include H2 only by default. Add H3s only when a section has more than three subsections and readers genuinely search at that level.
- Cap the visible list at around 10 to 12 items. Past that, make the rail internally scrollable with
max-heightandoverflow-y: auto. - Truncate long headings to one or two lines with an ellipsis and keep the full text in the
titleattribute.
Labels and micro-copy
- Use the real heading text. Do not invent shorter labels that no longer match the section the user lands on.
- Write headings that work as navigation in the first place: specific, front-loaded with the keyword, no clever wordplay.
- Header for the component: “On this page”, “In this article” or “Contents”. Keep it small and uppercase or muted.
Visual states
A sticky TOC has four states to design, and most implementations forget at least two:
- Default: muted text, generous line height (1.4 to 1.6), 8 to 12px vertical spacing between items.
- Hover: full contrast text plus a subtle background or underline.
- Active (scroll-spy): a coloured left border or filled marker plus full contrast and medium weight. Do not rely on colour alone.
- Visited or read: optional, a slightly dimmed state or a small tick for sections already passed. Useful on tutorials.
Add a thin vertical rail line behind the list. It visually groups the items and gives the active indicator something to sit on, which is the trick you see on the best documentation sites.
Step 3: Make it stick correctly
The CSS is short. The bugs are not.
.article-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 260px;
gap: 48px;
align-items: start; /* required, otherwise the rail stretches and never sticks */
}
.toc {
position: sticky;
top: 88px; /* height of your sticky header plus breathing room */
max-height: calc(100vh - 120px);
overflow-y: auto;
overscroll-behavior: contain;
}
/* stop anchors landing under the sticky header */
:is(h2, h3)[id] {
scroll-margin-top: 96px;
}
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
}
The three classic sticky bugs
- An ancestor has
overflow: hiddenoroverflow: auto. Sticky silently dies. Audit the whole ancestor chain. - The parent is not tall enough. Sticky only travels inside its parent box, so the rail must be a child of a container as tall as the article.
- Flex children stretch by default. Set
align-items: startoralign-self: starton the rail.
Step 4: Build scroll-spy highlighting that does not lie
Naive scroll listeners fire hundreds of times per second and highlight the wrong item near the page bottom. Use IntersectionObserver with a top-weighted root margin instead.
const links = document.querySelectorAll('.toc a');
const map = new Map();
links.forEach(a => {
const id = a.getAttribute('href').slice(1);
const section = document.getElementById(id);
if (section) map.set(section, a);
});
let visible = new Set();
const setActive = (link) => {
links.forEach(a => {
a.classList.remove('is-active');
a.removeAttribute('aria-current');
});
if (link) {
link.classList.add('is-active');
link.setAttribute('aria-current', 'true');
}
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) visible.add(entry.target);
else visible.delete(entry.target);
});
if (visible.size === 0) return;
const first = [...visible].sort(
(a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top
)[0];
setActive(map.get(first));
}, {
rootMargin: '-90px 0px -65% 0px', // header offset, then a band near the top
threshold: 0
});
map.forEach((link, section) => observer.observe(section));
Edge cases to handle
- Bottom of the page: when the user hits the footer, force the last item active, otherwise short final sections never highlight.
- Very short sections: observe the whole section wrapper, not just the heading element, or fast scrolling skips the highlight entirely.
- Click then scroll conflict: after a TOC click, lock the active state for around 400ms so the observer does not flicker through intermediate sections.
- Auto-scroll the rail: if the TOC itself scrolls, call
scrollIntoView({ block: 'nearest' })on the active link so it never sits off-screen. - URL hash: update it with
history.replaceStateon click so readers can copy a deep link, but avoid rewriting it on every scroll tick.

Step 5: Mobile fallbacks that actually get used
There is no rail on a 390px screen, so the desktop pattern has to be replaced, not shrunk. Ranked by reliability:
- Collapsible inline block under the intro. Use a native
<details>element labelled “In this article (9 sections)”. Zero JavaScript, works everywhere, and the item count sets expectations. - Compact sticky bar that appears after the intro. Shows “Contents: current section name” with a chevron that expands the full list. Keep it under 56px tall and hide it when the user scrolls up so it never fights the browser chrome.
- Bottom sheet from a labelled floating button. Best for guides over 3,000 words. Put it bottom right, above the thumb zone, with a text label rather than a bare icon.
- Reading progress bar as a companion. Two or three pixels at the top. It is not navigation, but it reduces the anxiety that drives abandonment on long pages.
Whatever you pick, never open a full-screen overlay without an obvious close affordance, and make sure tapping an item closes the panel and scrolls in one action.
Step 6: Accessibility essentials
- Wrap the list in
<nav aria-label='Table of contents'>and use a real<ol>or<ul>. - Mark the current item with
aria-current='true', not just a CSS class. - Ensure visible focus rings. The rail is keyboard reachable and skipping focus styles breaks it for keyboard users.
- Move focus to the target heading after a jump (give it
tabindex='-1') so screen reader and keyboard users are not left at the top of the document. - Respect
prefers-reduced-motionfor smooth scrolling. - Active state needs at least 3:1 contrast against the default state, plus a non-colour cue such as a border or weight change.

Step 7: Performance and layout stability
- Render the TOC server side where possible. A JavaScript-built TOC that appears late causes layout shift and hurts CLS.
- Reserve the rail width in CSS from the start, even while the list is empty.
- Generate heading IDs at build or publish time, not at runtime, so anchors are stable and shareable forever.
- Keep the observer to a single instance and disconnect it on page unload in SPA setups.
The SEO angle: what a sticky TOC does and does not do
Let us be precise, because this area is full of inflated claims.
Real benefits
- Explicit structure. Heading IDs plus anchor links make each passage addressable. That supports deep links from your own internal linking, newsletters and social posts.
- Possible jump-to-section links. Google can surface links to sections of a page when heading IDs and anchors are clean. It is not guaranteed and cannot be forced, but the technical prerequisites are exactly what a TOC gives you.
- Better internal linking. You can link from other articles directly to
/guide/#scroll-spyrather than the top of a 4,000 word page. - Engagement signals. Lower pogo-sticking on informational queries, because the reader sees straight away that their sub-question is covered.
- Discipline in writing. A TOC exposes weak outlines. If the list reads badly, the article is badly structured, and you fix it before publishing.
Myths to drop
- A TOC does not automatically produce sitelinks in the SERP.
- There is no ranking boost for the component itself. The benefit is structural and behavioural.
ItemListschema for a TOC is not a documented rich result path. Focus on clean HTML instead.
Sticky TOC patterns worth studying
- Developer documentation sites (MDN, Stripe, Tailwind): right rail, H2 plus H3, thin rail line with a coloured active marker, internal scroll when the list is long. The reference implementation.
- CSS-Tricks published one of the earliest widely copied writeups of a
position: stickyTOC with scrolling active states. Its lesson still holds: use the empty side column you already have. - Nielsen Norman Group pairs a persistent TOC with generous typography and treats it as a wayfinding device rather than decoration.
- Long product detail pages in e-commerce: a horizontal sticky TOC under the header, proving the pattern works even without a side rail.
- Personal design blogs such as Arun Venkatesan’s much-shared Dribbble concept: minimal type, heavy use of whitespace, active state as the only accent colour on screen.
What they all share: low visual weight, one accent colour, real heading text, and an active state you can read at a glance.

How to measure whether it works
Ship it as an A/B test or a before and after cohort comparison on your top 20 long-form URLs, then track:
| Metric | What to look for |
|---|---|
| TOC click rate | Share of readers using at least one link. 10 to 25 percent is common on guides |
| Most clicked item | Tells you the real intent. Consider promoting that section higher |
| Scroll depth at 50 and 90 percent | Should rise, even if average time on page dips slightly |
| CTA or conversion rate below the fold | The real test. More people reaching the bottom should mean more actions |
| CLS and INP | Must not regress after adding the component |
Pre-launch QA checklist
- Every H2 has a stable, human-readable ID.
- Anchors land with the heading fully visible under any sticky header.
- Active state is correct at the very top and the very bottom of the page.
- The rail never overlaps the footer or the article text at any breakpoint.
- Long TOCs scroll internally and auto-follow the active item.
- Keyboard tab order is logical and focus is visible.
- Mobile fallback is present, discoverable and closes on selection.
- No layout shift on load. The rail space is reserved in CSS.
- Print stylesheet hides the TOC or converts it to a static list.
- Analytics events fire on TOC clicks with the section label attached.
FAQ
Should a sticky table of contents be on the left or the right?
Right rail for editorial blogs, because it keeps the reading column optically centred and avoids competing with global navigation. Left rail suits documentation, where navigation is the primary task and users expect a hierarchical menu there.
How long does an article need to be to justify a TOC?
Roughly 1,000 to 1,200 words and at least four H2 sections. Below that, the outline is short enough to scan by scrolling.
Should I include H3 headings in the table of contents?
Only when the article is long and the subsections answer distinct questions. Two levels maximum, indented and visually lighter than the H2 entries. Three levels turns the rail into a sitemap. This guide goes deeper on it.
Can I build a sticky TOC without JavaScript?
Yes for the sticky behaviour and the anchor links: position: sticky plus scroll-margin-top covers it. Scroll-spy highlighting still needs a small script (or CSS scroll-driven animations, which are powerful but not yet safe as the only implementation across all browsers).
Does a table of contents hurt time on page?
Average session time can dip slightly because readers skip content they do not need, but the metrics that matter usually improve: scroll depth into the relevant section, return visits and conversions below the fold. Optimise for task completion, not for time spent hunting.
Does a sticky TOC affect Core Web Vitals?
It can, if it is injected by JavaScript after render. Build it server side, reserve the column width in CSS and use a single IntersectionObserver and the impact on CLS and INP stays effectively zero. Someone has put together a good summary of it.
What about AMP or feed readers?
Keep the TOC as semantic HTML in the article body so it degrades to a plain anchor list wherever sticky positioning or scripts are unavailable.
Final word
Good sticky table of contents design is mostly restraint: one accent colour, real heading text, an honest active state, a mobile fallback that is genuinely usable and anchors that land where they should. Build it server side, test it on your longest guides first, and let the click data tell you which sections your readers actually came for.