Automatic Table of Contents for WordPress Posts

book with a blue bookmark

Create an pop-put set of jump links based on your posts’ <h2> tags – a responsive, automatically generated table of contents for your WordPress posts. It’s a great way of enhancing the user experience of a blog site that’s got multiple subheadings (like this one)… because it does it automatically. You can see it in action on this site right now.

We’ll hide it on mobile devices so we don’t clutter the display, but on laptops & desktops, you can see it in the bottom-right.

We’ve set it up so it works nicely alongside our scroll-to-top tutorial too.

The key to this is that we don’t want to have to go back through all our blog posts and fill-in the “id” property of the <h2> element in every single post. That would take ages. So we’ll automate it.

importantBefore you start this tutorial, make sure you’re using a custom child theme so you can edit functions.php.

How It’ll Work : The Logic

The core logic for this extension will happen in the browser, using JavaScript (jQuery)…

  • Wait until the window has loaded.
  • Scan for all .entry-content h2 elements that have an id property set.
  • If there are zero h2 elements…
    • Scan for all .entry-content h2 elements with or without an id property.
  • Create a pull-out container <div> element and attach a tab/button so we can pull it out and collapse it.
  • For each matched heading element (if any)…
    • Create a list item with an <a> element inside that has the same text as the matched heading element. This is our jump link.
    • Store a handle to the matched heading in the button using a custom data property (data-target=...).
    • Add the new list item to the pull-out container.
  • Attach the pull-out container <div> to the main <body> element, give it a CSS position of “fixed” and a high z-index (to make it always on-top).
pull-out automatic table of contents for WordPress
Dynamic WP Table of Contents

Starting with Scaffolding Code

Start by creating some empty files and putting some basic structure in place. In your custom child theme, create a new file called wpt-auto-toc.php. Create a folder called “wpt-auto-toc” and create two empty text files in the new folder called “auto-toc-frontend.css” and “auto-toc-frontend.js”. Your child theme’s folder structure should look something like this:

  • parent-theme/
  • your-theme/
    • functions.php
    • style.css
    • wpt-auto-toc/
      • auto-toc-frontend.css
      • auto-toc-frontend.js
    • wpt-auto-toc.php

Open wpt-auto-toc.php and paste the following into it.

<?php

/**
 * WP Tutorials Auto Table of Contents
 *
 * https://wp-tutorials.tech/refine-wordpress/auto-toc/
 */

// Block direct access.
defined('WPINC') || die();

// Which post types should have table-of-contents?
const WPTTOC_POST_TYPES = array('post', 'page');
const WPTTOC_SELECTOR_WITH_ANCHORS = '#main .entry-content h2[id]';
const WPTTOC_SELECTOR_ALL_HEADINGS = '#main .entry-content > h2';
const WPTTOC_ANIM_DURATION = 1000; // ms.

function wpttoc_enqueue_scripts() {
	if (!is_single()) {
		// Don't show TOCs on archive/search/taxonomy pages.
	} elseif (empty($post_type = get_post_type())) {
		// Error.
	} elseif (!in_array($post_type, WPTTOC_POST_TYPES)) {
		// Don't show TOCs on post-types that aren't configured
		// in WPTTOC_POST_TYPES.
	} else {
		$base_url = get_stylesheet_directory_uri() . '/' . pathinfo(__FILE__, PATHINFO_FILENAME) . '/';
		$version = wp_get_theme()->get('Version');

		$frontend_args = array(
			'heading' => __('Quick Jump', 'wp-tutorials'),
			'anchorHeadingsSelector' => WPTTOC_SELECTOR_WITH_ANCHORS,
			'allHeadingsSelector' => WPTTOC_SELECTOR_ALL_HEADINGS,
			'scrollAnimDuration' => WPTTOC_ANIM_DURATION,
		);

		wp_enqueue_script('wpt-auto-toc', $base_url . 'auto-toc-frontend.js', null, $version, true);
		wp_localize_script(
			'wpt-auto-toc',
			'autoToc',
			$frontend_args
		);
		wp_enqueue_style('wpt-auto-toc', $base_url . 'auto-toc-frontend.css', null, $version);
	}
}
add_action('wp_enqueue_scripts', 'wpttoc_enqueue_scripts');

The code should be easy enough to read, and the key interesting function is wp_localize_script(). This is the recommended way of sending PHP variables to frontend JavaScript. In this instance, we’re asking WordPress to create a JavaScript variable called autoToc, which will be an object representation of the $frontend_args array.

The Styles

Open wpt-auto-toc/auto-toc-frontend.css and paste the following into it to get you started. It’s obviously set up for this site, but it’s easy enough to configure it for your project.

/**
 * WP Tutorials Auto Table of Contents
 *
 * https://wp-tutorials.tech/refine-wordpress/auto-toc/
 */
.wpttoc-container {
    display: none; /* Hide on mobile */
    position: fixed;
    bottom: 7.5em;
    font-size: 20pt;
    transition: 0.3s;
    z-index: 100;
    right: 0.5em;
    width: 7em;
}

@media(min-width: 920px) {
    .wpttoc-container {
        display: block; /* Show on desktop */
    }
}

.wpttoc-container.hidden {
    transform: translateX(6em);
}

.wpttoc-tab {
    font-size: 14pt;
    position: absolute;
    width: 2em;
    height: 3em;
    left: -2.5em;
    bottom: 0;
    border-radius: 0.25em;
    background-color: #1d2bbe;
    border: 2px solid black;
    cursor: pointer;
    transition: 0.3s;
}

.wpttoc-container .wpttoc-tab .show,
.wpttoc-container .wpttoc-tab .hide {
    display: block;
}

.wpttoc-container.visible .wpttoc-tab .show {
    display: none;
}

.wpttoc-container.hidden .wpttoc-tab .hide {
    display: none;
}

.wpttoc-tab i {
    position: absolute;
    color: white;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

.wpttoc-container h2 {
    font-size: 14pt;
    text-align: center;
    background-color: #ffffff80;
    padding: 0.5em 0;
    border-radius: 0.5em;
}

.wpttoc-container ul {
    font-size: 11pt;
    list-style-type: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5em;
}

.wpttoc-container li a {
    display: block;
    font-weight: bold;
    background-color: #1d2bbe;
    width: 100%;
    padding: 0.5em 1em;
    text-align: center;
    border-radius: 0.5em;
    border: 2px solid black;
    color: white;

}

.wpttoc-tab:hover,
.wpttoc-container li a:hover {
    filter: brightness(130%);
}

JavaScript Code – The Big Bit

This is the big bit… Paste the following into wpt-auto-toc/auto-toc-frontend.js and have a look through the comments/sections to see what’s going on.

/**
 * WP Tutorials Auto Table of Contents
 * 
 * https://wp-tutorials.tech/refine-wordpress/table-of-contents-for-wordpress-posts/
 */
(function($) {
    'use strict';

    $(window).on('load', function() {
        if (typeof autoToc != 'undefined') {

            console.log('WPT Auto-TOC : load');

            const scrollPercentageThreshold = 0.10;
            const scrollPixelThreshold = 300;

            autoToc.container = null;
            autoToc.list = null;
            autoToc.isAutoHideEnabled = true;

            var headings = null;
            var anchorHeadings = $(autoToc.anchorHeadingsSelector);
            if (anchorHeadings.length > 0) {
                headings = anchorHeadings;
            }
            else {
                headings = $(autoToc.allHeadingsSelector);
            }

            /**
             * For each of the heading elements we've got, create
             * '<li><a>LABEL</a></li>' and add it to a <ul> within our pull-out
             * container.
             */
            headings.each(function(index, element) {
                // If the pullout container doesn't exist, create it now.
                if (!autoToc.container) {
                    autoToc.container = createTocContainer(autoToc.heading);
                    autoToc.list = $(`<ul></ul>`);
                }

                var labelText = $(element).text();
                var listItem = $('<li></li>');
                var button = $(`<a href="javascript:void(0);">${labelText}</a>`);

                // Store a handle the the discovered h2 in our button, so we
                // can scroll to it in the click handler.
                $(button).data('target', element);

                button.click(function() {
                    var heading = $(this).data('target');
                    
                    autoToc.isAutoHideEnabled = false;

                    $('html, body').animate({
                            scrollTop: $(heading).offset().top - $(heading).height()
                        },
                        autoToc.scrollAnimDuration
                    );
                });

                $(listItem).append(button);
                $(autoToc.list).append(listItem);
            });

            /**
             * If we have created a pull-out container then add it to the body
             * element now, and listen for the window's scroll event.
             */
            if (autoToc.container) {
                $(autoToc.container).append(autoToc.list);
                $("body").append(autoToc.container);


                // Auto-hide the TOC if we scroll down the page without
                // interacting with it. Keep out of the way.
                $(window).on('scroll', function() {
                    var offset = $(this).scrollTop();
                    var total = document.body.scrollHeight;

                    if (!autoToc.isAutoHideEnabled) {
                        // Do nothing.
                    } else {
                        var isPastThreshold = false;
                        if ((1.0 * offset / total) > scrollPercentageThreshold) {
                            isPastThreshold = true;
                        } else if (offset > scrollPixelThreshold) {
                            isPastThreshold = true;
                        } else {
                            // ...
                        }

                        if( isPastThreshold ) {
                            hideAutoToc();
                        }
                    }
                });
            }

            /**
             * Create the slide-out container, with a "tab" to toggle its state,
             * and a h2 heading element.
             */
            function createTocContainer(heading) {
                var container = $(`<div class="wpttoc-container visible"></div>`);

                var tab = $(`<div class="wpttoc-tab"><i class="fas fa-chevron-left show"></i><i class="fas fa-chevron-right hide"></i></div>`);
                tab.click(function() {
                    toggleAutoToc();
                });
                $(container).append(tab);

                $(container).append(`<h2>${heading}</h2`);

                return container;
            }

            /**
             * Toggle the pull-out panels' visibility.
             */
            function toggleAutoToc() {
                if ($(autoToc.container).hasClass('hidden')) {
                    showAutoToc();
                } else {
                    hideAutoToc();
                }
            }

            function showAutoToc() {
                $(autoToc.container).removeClass('hidden');
                $(autoToc.container).addClass('visible');
            
                autoToc.isAutoHideEnabled = false;
            }

            function hideAutoToc() {
                $(autoToc.container).removeClass('visible');
                $(autoToc.container).addClass('hidden');
                
                autoToc.isAutoHideEnabled = false;
            }
        }
    });
})(jQuery);

Break it Down

Near the top, we check to see that autoToc has been passed from the back-end to the front-end. It’s a just-in-case check, because it should always be present.

Then we set up some constants and variables and start looking for headings. First, we query all headings with an id property. If that query doesn’t return a length greater than zero (i.e. there are no headings with anchors) then we run a second query to check all headings (with or without anchors).

Then we just loop through the matched headings, and on the first run through the loop we check to see if we need to create the pull-out container <div>. This way, if headings.length is zero (so we don’t run through the loop even once), we don’t needlessly create the pull-out container.

After the main loop, we check to see if we’ve created the container <div> and stored it in autoToc.container. If so, we attach it to the root <body> element and set up the scroll handler so we can auto-hide the TOC when we scroll down.

It might look like a big lump of code, but it breaks down into easy-to-follow chunks and functions.

Deploy & Test

To enable the code, we need to require_once “wpt-auto-toc.php”, pulling it into the custom child theme. Open your child theme’s functions.php file and add the following to it:

// WP Tutorials Auto Table of Contents
require_once dirname(__FILE__) . '/wpt-auto-toc.php';

Save everything and load some content on your site that’s got a bunch of <h2> elements on it. You should see a shiny new pull-out table of contents. Nice 😎 😎 😎

If nothing’s happening, open your browser’s Dev Tools and check the JavaScript Console for messages. You should at least see the result of our call to console.log() as soon as our code starts to run.

JavaScript console
JavaScript Console

Making Changes

The main things you’ll want to tinker with are the jQuery element selectors. These are in wpt-auto-toc.php, held in WPTTOC_SELECTOR_WITH_ANCHORS and WPTTOC_SELECTOR_ALL_HEADINGS. They’re added into $frontend_args and passed into the frontend as part of the autoToc object.

The scroll-from-top thresholds for auto-hiding the TOC container are held near the top of auto-toc-frontend.js but you can move this into wpt-auto-toc.php and pass them to the frontend in autoToc, to keep things nice and tidy.

Have fun auto-generating TOCs for your WordPress blogs! 🏆 👍

Like This Tutorial?

Let us know

WordPress plugins for developers

Leave a comment