How to Add a Table of Contents to WordPress Blog Posts Without a Plugin

If you run a WordPress blog, you already know the drill: every long-form article needs a table of contents to help readers navigate, improve dwell time, and pick up those juicy sitelinks in Google search results. The lazy solution is to install a plugin. The smart solution is to build it yourself.

In this tutorial, we will show you how to add a table of contents in WordPress without a plugin by parsing your H2 and H3 headings automatically with a few lines of PHP and a sprinkle of JavaScript. No bloat, no third-party scripts, no performance hit.

Why Build a Table of Contents Without a Plugin?

Most tutorials on the first page of Google will tell you to manually create anchor tags and lists for every post. That works, but it is tedious and error-prone. Others will push you toward heavy plugins like Easy Table of Contents. Here is why the DIY approach wins in 2026:

  • Performance: No extra HTTP requests, no jQuery dependency, no admin bloat.
  • Full control: You decide the markup, the styling, and the behavior.
  • SEO friendly: Clean semantic HTML with proper anchor links helps Google generate jump-to links in the SERP.
  • No plugin conflicts: One less thing to update or break during a core WordPress upgrade.
  • Reusable: Once written, it works on every post automatically.
laptop code wordpress

How It Works: The Logic Behind Automatic TOC Generation

The idea is simple. Before WordPress prints your post content on the page, we intercept it with a filter, scan it for H2 and H3 tags, inject unique IDs into those headings, and build a nested list of links. Then we prepend that list to the content.

Here is a quick overview of the pieces involved:

Component Role
PHP function in functions.php Parses headings, injects IDs, generates the TOC list
WordPress filter the_content Applies the function to every single post
CSS in your theme stylesheet Styles the TOC box
Vanilla JavaScript (optional) Adds smooth scroll and collapse behavior

Step 1: Add the PHP Function to Your Theme

Open the functions.php file of your child theme (never edit the parent theme directly). Paste the following code at the bottom:

function expressjs_generate_toc($content) {
    if (!is_single() || !in_the_loop() || !is_main_query()) {
        return $content;
    }

    // Match all H2 and H3 tags
    preg_match_all('/<h([2-3])(.*?)>(.*?)<\/h[2-3]>/i', $content, $matches, PREG_SET_ORDER);

    if (count($matches) < 3) {
        return $content; // Skip TOC if fewer than 3 headings
    }

    $toc = '<div class="expressjs-toc" id="toc">';
    $toc .= '<p class="toc-title">Table of Contents</p>';
    $toc .= '<ul>';

    $current_level = 2;
    foreach ($matches as $match) {
        $level = (int) $match[1];
        $title = strip_tags($match[3]);
        $anchor = sanitize_title($title);

        // Inject the ID into the heading inside the content
        $new_heading = '<h' . $level . ' id="' . $anchor . '"' . $match[2] . '>' . $match[3] . '</h' . $level . '>';
        $content = str_replace($match[0], $new_heading, $content);

        if ($level == 3 && $current_level == 2) {
            $toc .= '<ul>';
            $current_level = 3;
        } elseif ($level == 2 && $current_level == 3) {
            $toc .= '</ul>';
            $current_level = 2;
        }

        $toc .= '<li><a href="#' . $anchor . '">' . $title . '</a></li>';
    }

    if ($current_level == 3) {
        $toc .= '</ul>';
    }

    $toc .= '</ul></div>';

    return $toc . $content;
}
add_filter('the_content', 'expressjs_generate_toc');

What this code does, step by step:

  1. Runs only on single posts inside the main loop.
  2. Uses a regex to grab every H2 and H3 in the post content.
  3. Skips generation if the post has fewer than 3 headings (short articles do not need a TOC).
  4. Creates a slug for each heading using sanitize_title().
  5. Injects an id attribute into each heading so anchor links work.
  6. Builds a nested unordered list and prepends it to the content.
laptop code wordpress

Step 2: Style the Table of Contents with CSS

Add the following CSS to your theme’s style.css or the WordPress Customizer under Additional CSS:

.expressjs-toc {
    background: #f7f9fc;
    border-left: 4px solid #0d6efd;
    padding: 20px 25px;
    margin: 30px 0;
    border-radius: 6px;
    font-size: 15px;
}
.expressjs-toc .toc-title {
    font-weight: 700;
    font-size: 18px;
    margin: 0 0 12px 0;
}
.expressjs-toc ul {
    list-style: none;
    padding-left: 15px;
    margin: 0;
}
.expressjs-toc ul ul {
    padding-left: 20px;
    margin-top: 6px;
}
.expressjs-toc li {
    margin: 6px 0;
}
.expressjs-toc a {
    text-decoration: none;
    color: #0d6efd;
}
.expressjs-toc a:hover {
    text-decoration: underline;
}

Feel free to adjust the colors to match your brand. This minimal style keeps the TOC readable without shouting for attention.

Step 3: Add Smooth Scroll with Vanilla JavaScript

Modern browsers support smooth scrolling natively via CSS, but a tiny script gives you more control (like offsetting for a sticky header). Add this to your theme’s JS file or drop it inline before the closing body tag:

document.addEventListener('DOMContentLoaded', function() {
    const links = document.querySelectorAll('.expressjs-toc a');
    links.forEach(link => {
        link.addEventListener('click', function(e) {
            e.preventDefault();
            const target = document.querySelector(this.getAttribute('href'));
            if (target) {
                const offset = 80; // adjust for sticky header height
                const top = target.getBoundingClientRect().top + window.pageYOffset - offset;
                window.scrollTo({ top: top, behavior: 'smooth' });
                history.pushState(null, null, this.getAttribute('href'));
            }
        });
    });
});

That is it. Publish a post with a few H2 and H3 tags and reload it. You should see a clean, automatic table of contents at the top.

laptop code wordpress

Optional Improvements

Make the TOC Collapsible

If your posts are long, users may want to hide the TOC. Wrap the list in a details element for a native accordion with zero JavaScript:

<details open>
  <summary>Table of Contents</summary>
  <ul>...</ul>
</details>

Add a Floating Sidebar TOC

For desktop readers, a sticky TOC on the left side is a huge UX win. Use CSS position: sticky with a top offset and hide it on mobile with a media query.

Support H4 Headings

Simply update the regex pattern from [2-3] to [2-4] and add another nesting level in the loop.

Common Pitfalls to Avoid

  • Do not use it on pages with existing anchor plugins. You will get duplicate IDs.
  • Escape HTML entities inside heading text. If your titles include quotes or ampersands, use esc_html() when building the anchor.
  • Test with caching plugins. Clear your cache after editing functions.php or the TOC may not appear until the cache regenerates.
  • Back up before editing functions.php. A missing semicolon can take your site down.
laptop code wordpress

Performance Comparison: Plugin vs. Custom Code

Metric Popular TOC Plugin Custom PHP Code
Extra HTTP requests 2 to 4 0
Additional CSS loaded 15 to 40 KB Under 1 KB
Additional JavaScript 20 to 60 KB Under 1 KB
Admin settings pages Yes None
Update maintenance Monthly Never

Frequently Asked Questions

Will this work with the Gutenberg block editor?

Yes. The filter runs on the final HTML output after Gutenberg has rendered the blocks, so it does not matter whether you use classic editor or blocks.

Does the table of contents help SEO?

Absolutely. Google often picks up anchor links from a well-structured TOC and displays them as jump-to links directly in the search results, which increases your CTR. It also improves dwell time by helping users find what they need faster.

Will this break if I use a page builder like Elementor or Bricks?

The filter targets the_content, so it works with any builder that outputs headings through the standard WordPress content flow. If your builder bypasses the_content, you may need to hook into the builder-specific filter instead.

Can I put the TOC somewhere other than the top of the post?

Yes. Instead of prepending the TOC to the content, look for a placeholder shortcode like [toc] inside the content and replace it with the generated list using str_replace().

Is this safe from a security standpoint?

The code uses WordPress core functions like sanitize_title() and only outputs heading text that was already in your own post. For added safety, wrap the final title output in esc_html().

Wrapping Up

Building a table of contents in WordPress without a plugin is not just a nerdy exercise, it is a legitimate performance and UX upgrade. With around 40 lines of PHP and a few lines of CSS, you get a lightweight, maintainable, and SEO-friendly TOC that scales across every post on your site.

Drop this code into your child theme today and enjoy faster pages, happier readers, and one fewer plugin cluttering your dashboard. If you want to push it further, wire it up to Intersection Observer for a scrollspy effect that highlights the active section as users read. Happy coding.

Recent Posts

No Posts Found!

Categories

Tags

    Subscribe

    You have been successfully Subscribed! Ops! Something went wrong, please try again.

    About Us

    Express Jam Studio was founded in 2004 by John Smith. John had previously worked for a courier company, but he saw an opportunity to start his own business in the web design and development industry.

    Contact Info

    Copyright © 2022 Express Jam Studio. All Rights Reserved.