Skip to main content
Settings
Search
Appearance
Theme Mode
About
Jekyll v3.10.0
Environment Production
Last Build
2026-04-29 13:38 UTC
Current Environment Production
Build Time Apr 29, 13:38
Jekyll v3.10.0
Build env (JEKYLL_ENV) production
Page Location
Page Info
Layout default
Collection docs
Path _docs/features/toc.md
URL /docs/features/toc/
Date 2026-04-29
Theme Skin
SVG Backgrounds
Layer Opacity
0.6
0.04
0.08

Table of Contents

Automatic table of contents generation from page headings with active section highlighting.

Overview

  • Auto-Generated: Extracts from h2-h6 headings
  • Scroll Spy: Highlights current section
  • Smooth Scroll: Animated navigation
  • Responsive: Sidebar on desktop, offcanvas on mobile

Implementation

Include Template

{% include content/toc.html %}

TOC Generation

The toc.html include uses Kramdown’s built-in TOC:

<nav id="TableOfContents" class="toc">
  <h2 class="toc-title">On This Page</h2>
  {{ content | toc_only }}
</nav>

Or manual extraction:

<nav id="TableOfContents">
  <ul class="toc-list">
    {% for heading in page.content | split: '<h' %}
      {% if heading contains 'id="' %}
        {% assign id = heading | split: 'id="' | last | split: '"' | first %}
        {% assign level = heading | slice: 0, 1 %}
        {% assign text = heading | split: '>' | last | split: '<' | first %}
        <li class="toc-item toc-level-{{ level }}">
          <a href="#{{ id }}" class="toc-link">{{ text }}</a>
        </li>
      {% endif %}
    {% endfor %}
  </ul>
</nav>

Configuration

Enable TOC

In front matter:

---
toc: true
---

Or site-wide in _config.yml:

defaults:
  - scope:
      type: docs
    values:
      toc: true

Heading Levels

Configure which headings appear:

toc:
  min_level: 2  # Start at h2
  max_level: 4  # End at h4

Styling

Basic Styles

.toc {
  position: sticky;
  top: 80px;
  max-height: calc(100vh - 100px);
  overflow-y: auto;
}

.toc-title {
  font-size: 0.875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 1rem;
}

.toc-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.toc-link {
  display: block;
  padding: 0.25rem 0;
  color: var(--bs-secondary);
  text-decoration: none;
  font-size: 0.875rem;
  border-left: 2px solid transparent;
  padding-left: 0.75rem;
}

.toc-link:hover {
  color: var(--bs-primary);
}

.toc-link.active {
  color: var(--bs-primary);
  border-left-color: var(--bs-primary);
  font-weight: 500;
}

Nested Levels

.toc-level-3 {
  padding-left: 1rem;
}

.toc-level-4 {
  padding-left: 2rem;
  font-size: 0.8125rem;
}

Scroll Spy

Intersection Observer

function initScrollSpy() {
  const headings = document.querySelectorAll('h2[id], h3[id], h4[id]');
  const tocLinks = document.querySelectorAll('.toc-link');
  
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          tocLinks.forEach((link) => link.classList.remove('active'));
          const activeLink = document.querySelector(
            `.toc-link[href="#${entry.target.id}"]`
          );
          activeLink?.classList.add('active');
        }
      });
    },
    { rootMargin: '-20% 0% -70% 0%' }
  );
  
  headings.forEach((heading) => observer.observe(heading));
}

Smooth Scrolling

CSS Method

html {
  scroll-behavior: smooth;
}

JavaScript Method

document.querySelectorAll('.toc-link').forEach((link) => {
  link.addEventListener('click', (e) => {
    e.preventDefault();
    const targetId = link.getAttribute('href').slice(1);
    const target = document.getElementById(targetId);
    const headerOffset = 80;
    const position = target.offsetTop - headerOffset;
    
    window.scrollTo({
      top: position,
      behavior: 'smooth'
    });
    
    history.pushState(null, '', `#${targetId}`);
  });
});

Responsive Behavior

Desktop

TOC appears in right sidebar:

<aside class="d-none d-lg-block">
  {% include content/toc.html %}
</aside>

Mobile

TOC in offcanvas (see Mobile TOC):

<div class="offcanvas offcanvas-end d-lg-none" id="tocSidebar">
  {% include content/toc.html %}
</div>

Accessibility

ARIA Attributes

<nav id="TableOfContents" 
     aria-label="Table of contents"
     role="navigation">

Keyboard Navigation

  • Tab through TOC links
  • Enter to navigate to section
  • Focus moves to heading

Troubleshooting

TOC Not Generating

  1. Verify headings have IDs
  2. Check toc: true in front matter
  3. Ensure Kramdown processor

Scroll Spy Not Working

  1. Check heading IDs match TOC hrefs
  2. Verify Intersection Observer support
  3. Test observer margins

Styling Issues

  1. Check sticky positioning
  2. Verify z-index
  3. Test overflow behavior

See also

  • [[Features]]
  • [[Mobile TOC Floating Action Button]]
  • [[Sidebar Navigation System]]