Home Blog How to Add a Table of Contents to Jekyll Posts
Tutorial

How to Add a Table of Contents to Jekyll Posts

Add an automatic table of contents to your Jekyll posts — using kramdown's built-in TOC, the jekyll-toc plugin, or a JavaScript approach with active section highlighting.

How to Add a Table of Contents to Jekyll Posts

A table of contents helps readers navigate long posts and signals to Google that your content is structured and comprehensive. Jekyll has three ways to add one — from a one-line kramdown shortcut to a fully interactive sticky sidebar TOC.


Option 1: kramdown Built-in TOC (Simplest)

Jekyll uses kramdown as its default Markdown processor, and kramdown includes a built-in TOC feature that requires zero plugins.

Add TOC to a Specific Post

In your post Markdown, add this wherever you want the TOC to appear:

* TOC
{:toc}

That’s it. kramdown automatically generates a nested list from all headings in the post.

Output example:

  • Introduction
  • Setting Up
    • Installation
    • Configuration
  • Advanced Usage
  • Conclusion

Style It

The TOC is rendered as a plain <ul>. Add CSS to make it look polished:

// _sass/components/_toc.scss
.toc {
  background: var(--card-bg);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-md);
  padding: 1.25rem 1.5rem;
  margin-bottom: 2rem;
  font-size: 0.9rem;

  &::before {
    content: "On this page";
    display: block;
    font-weight: 700;
    margin-bottom: 0.75rem;
    color: var(--heading-color);
  }

  ul {
    margin: 0;
    padding-left: 1.25rem;
    list-style: none;
  }

  li {
    margin: 0.25rem 0;
  }

  a {
    color: var(--text-muted);
    text-decoration: none;

    &:hover {
      color: var(--link-color);
    }
  }
}

Add the class to the TOC element by wrapping it:

<div class="toc" markdown="1">
* TOC
{:toc}
</div>

Skip Specific Headings

To exclude a heading from the TOC, add {: .no_toc} after it:

## This heading appears in the TOC

## This one does not {: .no_toc}

Exclude the TOC from H1 and Only Show H2/H3

In _config.yml:

kramdown:
  toc_levels: "2..3"

Option 2: jekyll-toc Plugin (More Control)

The jekyll-toc plugin gives you more control — inject TOC separately from content, use it in layouts, and customise the output.

Install

# Gemfile
gem "jekyll-toc"
# _config.yml
plugins:
  - jekyll-toc

Run bundle install.

Usage in Layouts

In _layouts/post.html, split the content into TOC and body:


{% if page.toc %}
<aside class="post-toc">
  {{ content | toc_only }}
</aside>
{% endif %}

<div class="post-content">
  {{ content | inject_anchors }}
</div>

toc_only extracts the TOC as a standalone block.
inject_anchors adds id attributes to headings so TOC links work.

Control Per Post with Front Matter

---
toc: true   # Show TOC on this post
---
---
toc: false  # No TOC
---

Set a default in _config.yml:

defaults:
  - scope:
      type: posts
    values:
      toc: false   # Off by default, enable per post

Option 3: JavaScript TOC with Active Section Highlighting

For a sidebar TOC that highlights the current section as the user scrolls — the pattern used by documentation sites like Just the Docs and MDN:

HTML Structure

In your post layout, add a two-column wrapper:


<div class="post-wrapper">
  <article class="post-content">
    {{ content }}
  </article>
  
  <aside class="post-toc-sidebar" id="toc-sidebar">
    <nav aria-label="Table of contents">
      <p class="toc-sidebar__title">On this page</p>
      <ul id="toc-list"></ul>
    </nav>
  </aside>
</div>

JavaScript: Auto-Build + Scroll Spy

// assets/js/toc.js
(function() {
  const tocList = document.getElementById('toc-list');
  if (!tocList) return;

  // Build TOC from headings
  const headings = document.querySelectorAll('.post-content h2, .post-content h3');
  if (headings.length < 3) {
    document.getElementById('toc-sidebar').style.display = 'none';
    return;
  }

  headings.forEach(heading => {
    // Ensure heading has an ID
    if (!heading.id) {
      heading.id = heading.textContent
        .toLowerCase()
        .replace(/[^\w\s-]/g, '')
        .replace(/\s+/g, '-');
    }

    const li = document.createElement('li');
    li.className = heading.tagName === 'H3' ? 'toc-item toc-item--h3' : 'toc-item';
    li.innerHTML = `<a href="#${heading.id}" class="toc-link">${heading.textContent}</a>`;
    tocList.appendChild(li);
  });

  // Scroll spy — highlight active section
  const observer = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      const id = entry.target.getAttribute('id');
      const link = tocList.querySelector(`a[href="#${id}"]`);
      if (!link) return;

      if (entry.isIntersecting) {
        tocList.querySelectorAll('.toc-link').forEach(l => l.classList.remove('active'));
        link.classList.add('active');
      }
    });
  }, {
    rootMargin: '-10% 0px -80% 0px'
  });

  headings.forEach(h => observer.observe(h));
})();

CSS

.post-wrapper {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;

  @media (min-width: 1200px) {
    grid-template-columns: 1fr 240px;
    align-items: start;
  }
}

.post-toc-sidebar {
  display: none;

  @media (min-width: 1200px) {
    display: block;
    position: sticky;
    top: 2rem;
    font-size: 0.85rem;
  }
}

.toc-sidebar__title {
  font-weight: 700;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
  margin-bottom: 0.75rem;
}

#toc-list {
  list-style: none;
  padding: 0;
  margin: 0;
  border-left: 2px solid var(--border-color);
}

.toc-item {
  padding: 0.2rem 0;
}

.toc-item--h3 .toc-link {
  padding-left: 1.5rem;
}

.toc-link {
  display: block;
  padding: 0.2rem 0 0.2rem 0.75rem;
  color: var(--text-muted);
  text-decoration: none;
  transition: color 0.15s, border-color 0.15s;
  border-left: 2px solid transparent;
  margin-left: -2px;
  line-height: 1.4;

  &:hover { color: var(--link-color); }

  &.active {
    color: var(--link-color);
    border-left-color: var(--link-color);
    font-weight: 600;
  }
}

Which Option to Choose

  kramdown built-in jekyll-toc plugin JavaScript
Setup effort Minimal Easy Medium
Position Inline in post Flexible Sidebar
Scroll highlighting No No Yes
Works on GitHub Pages Yes No* Yes
Best for Quick inline TOC Layout-based TOC Documentation, long posts

*jekyll-toc requires GitHub Actions for deployment.

For most blogs: kramdown’s built-in {:toc} is the right choice — one line, zero dependencies.

For documentation or long technical posts: the JavaScript sidebar TOC with scroll highlighting is worth the extra setup.


Browse Jekyll themes on JekyllHub — themes like Just the Docs and Chirpy include TOC functionality built in.

Share LinkedIn