Auto-Generated Table of Contents
Automatically generate a sticky table of contents from your Ghost post headings with smooth scroll and active section highlighting.
Auto-Generated Table of Contents
Add an auto-generated table of contents to your Ghost posts that builds itself from your H2 and H3 headings. Includes smooth scrolling, active section highlighting, and a collapsible toggle for mobile.
The Code
Paste this into your Ghost site’s Settings → Code injection → Site footer:
<script>(function() { var article = document.querySelector('.gh-content, .post-content, article'); if (!article) return;
var headings = article.querySelectorAll('h2, h3'); if (headings.length < 3) return;
// Add IDs to headings that don't have them var usedIds = {}; headings.forEach(function(h) { if (!h.id) { var slug = h.textContent.trim().toLowerCase() .replace(/[^a-z0-9\s-]/g, '') .replace(/\s+/g, '-') .replace(/-+/g, '-'); if (usedIds[slug]) { slug = slug + '-' + usedIds[slug]; } usedIds[slug] = (usedIds[slug] || 0) + 1; h.id = slug; } });
// Build TOC var nav = document.createElement('nav'); nav.id = 'toc'; nav.setAttribute('aria-label', 'Table of contents');
var toggleBtn = document.createElement('button'); toggleBtn.textContent = 'Contents'; toggleBtn.setAttribute('aria-expanded', 'true'); toggleBtn.style.cssText = 'display:block;width:100%;padding:10px 16px;background:none;border:none;' + 'font-weight:600;font-size:14px;cursor:pointer;text-align:left;color:inherit;';
var list = document.createElement('ol'); list.style.cssText = 'list-style:none;padding:0;margin:0;';
headings.forEach(function(h) { var li = document.createElement('li'); var a = document.createElement('a'); a.href = '#' + h.id; a.textContent = h.textContent; a.style.cssText = 'display:block;padding:4px 16px;font-size:13px;color:inherit;' + 'text-decoration:none;opacity:0.7;transition:opacity 0.2s;' + 'border-left:2px solid transparent;';
if (h.tagName === 'H3') { a.style.paddingLeft = '32px'; a.style.fontSize = '12px'; }
a.addEventListener('mouseenter', function() { a.style.opacity = '1'; }); a.addEventListener('mouseleave', function() { if (!a.classList.contains('active')) a.style.opacity = '0.7'; });
a.addEventListener('click', function(e) { e.preventDefault(); document.getElementById(h.id).scrollIntoView({ behavior: 'smooth' }); history.pushState(null, '', '#' + h.id); });
li.appendChild(a); list.appendChild(li); });
toggleBtn.addEventListener('click', function() { var expanded = toggleBtn.getAttribute('aria-expanded') === 'true'; toggleBtn.setAttribute('aria-expanded', String(!expanded)); list.style.display = expanded ? 'none' : 'block'; });
nav.appendChild(toggleBtn); nav.appendChild(list);
nav.style.cssText = 'position:fixed;top:100px;right:20px;width:220px;max-height:calc(100vh - 140px);' + 'overflow-y:auto;background:var(--background-color,#fff);' + 'border:1px solid rgba(0,0,0,0.1);border-radius:8px;' + 'box-shadow:0 2px 8px rgba(0,0,0,0.08);z-index:100;' + 'font-family:inherit;';
document.body.appendChild(nav);
// Active section highlighting var links = list.querySelectorAll('a'); var observer = new IntersectionObserver(function(entries) { entries.forEach(function(entry) { if (entry.isIntersecting) { links.forEach(function(link) { link.classList.remove('active'); link.style.opacity = '0.7'; link.style.borderLeftColor = 'transparent'; }); var active = list.querySelector('a[href="#' + entry.target.id + '"]'); if (active) { active.classList.add('active'); active.style.opacity = '1'; active.style.borderLeftColor = 'var(--ghost-accent-color,#3b82f6)'; } } }); }, { rootMargin: '-80px 0px -80% 0px' });
headings.forEach(function(h) { observer.observe(h); });
// Hide on small screens var mq = window.matchMedia('(max-width: 1280px)'); function handleResize(e) { nav.style.display = e.matches ? 'none' : 'block'; } handleResize(mq); mq.addEventListener('change', handleResize);})();</script>How to Install
- In Ghost Admin, go to Settings → Code injection
- Paste the code into the Site footer section
- Click Save
- Open any post with 3+ headings — the TOC appears on the right side
The TOC only renders on posts with 3 or more H2/H3 headings. Posts with fewer headings won’t show it.
How It Works
The script scans the post content for h2 and h3 elements and builds a navigation list. If a heading doesn’t already have an id attribute, the script generates a URL-friendly slug from the heading text (e.g., “Setting Up Email” becomes #setting-up-email). This produces human-readable anchor links that work in shared URLs. An IntersectionObserver tracks which heading is currently visible and highlights the corresponding TOC link with a left border accent.
On screens narrower than 1280px, the TOC hides automatically to avoid overlapping the content. The toggle button lets readers collapse the TOC on desktop if they prefer a clean reading view.
Smooth scrolling uses the native scrollIntoView API with behavior: 'smooth' — no jQuery or external libraries needed.
Customization
Change position: Modify right:20px to left:20px for a left-aligned TOC. Adjust top:100px if your theme’s header is taller.
Change breakpoint: Replace 1280px with a different value to control when the TOC hides. Use 1024px for a more aggressive hide, or 1440px to show it on wider screens only.
Change accent color: The active link uses var(--ghost-accent-color,#3b82f6). Override the fallback color (#3b82f6) with your brand color if your theme doesn’t set --ghost-accent-color.
Luxe Themes