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.
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.