How to add Automatic Table of Contents to WordPress

How to add Automatic Table of Contents to WordPress

Share on facebook
Share on twitter
Share on pinterest

Recently I added the automatic table of contents to our longer posts. You can see how it looks like after this paragraph. Its code is fairly minimal, and since it’s automatic, I don’t need to bother with its maintenance. I’ll guide you through what it does so you don’t have to dig into our minified code if you want to steal it. What you see here is the first working draft tailored just for our needs. It could be perfected into a plugin, but I ask you not to do that, as we have plans. Here is how it looks like:

The automatic table of contents concept

It’s so simple it hurts. In long articles, if I want the table of contents I add this shortcode:


That’s it. It auto-collects headings up to the specified level and creates a link to them as a list. You know how it goes: it’s a shortcode registered via PHP, the whole magic happens in JS, and the looks come from CSS. I’ll break down each language so you can see what I mean.

Shortcode PHP

This one creates a div with two settings as data-attributes. The stopat specifies a heading level (from h1 to h6) up to which you want to see in the table of contents. I’ve found that if it drills down into every little subheading, the list can appear very long and I dislike that look. For example, I don’t want to include the poll. The offset is to adjust the scroll position in pixels. On this site, we want to scroll 20px above where it would typically scroll to (the precise top of a heading). It’s a small detail, but it feels better. Also, this could counter a sticky menu if we had one. Both are omittable for more comfortable use, as they have defaults.

function lwp_4644_autoc($atts) {
    if (empty($atts)) {
        $atts = array();
    if (empty($atts['stopat'])) {
        $atts['stopat'] = 'h4';
    if (empty($atts['offset'])) {
        $atts['offset'] = '20';
    return '<div class="autoc" data-stopat="'.$atts['stopat'].'" data-offset="'.$atts['offset'].'"></div>';
add_shortcode('autoc', 'lwp_4644_autoc');

The only WordPress function used here is add_shortcode() which is very basic. The full shortcode with non-default options would look like this:

[autoc stopat=h3 offset=30]

JS core code

Oh, the heart of the whole thing. It builds a list of every required heading. The real headings and their table of contents versions connect by slugs (hence the slugify part). That slug could be later used for #permalinks, but I decided to skip that feature. So it doesn’t yet manipulate the URLs. Clicks on links in the list do an animated scroll to the paired heading, simple as that. The focus was on automation, so I don’t need to maintain a table of contents by hand.

;(function($) {
        var $autoc = $(".autoc");
        var $content = $autoc.parent();
        var stopAt = $"stopat");
        var hs = [];
            case "h6":
            case "h5":
            case "h4":
            case "h3":
            case "h2":
        hs = hs.join();
        var $heads = $content.find(hs);
        if($heads.length === 0){
        var toc = "";
        toc += "<h2>Table of Contents</h2><ul>";
            var $this = $(this);
            var tag = $this[0].tagName;
            var txt = $this.text();
            var slug = slugify(txt);
            toc += '<li class="autoc-level-'+tag+'">';
            toc += '<a href="#" data-linkto="'+slug+'">'+txt+"</a></li>";
        toc += "</ul>";
        $(".autoc ul").on("click", "a", function(e){
            $([document.documentElement, document.body]).animate({
                scrollTop: $content.find('[data-linked="'+$(this)
                    .top - parseInt($autoc.attr("data-offset"), 10)
            }, 2000);
    function slugify(text){
        return text.toString().toLowerCase()
            .replace(/\s+/g, "-")           // Replace spaces with -
            .replace(/[^\w\-]+/g, "")       // Remove all non-word chars
            .replace(/\-\-+/g, "-")         // Replace multiple - with single -
            .replace(/^-+/, "")             // Trim - from start of text
            .replace(/-+$/, "");            // Trim - from end of text

As you can see, jQuery is a dependency, but of course, the same could be done without it. Our site already has it, so why not use it? It was easier 🙂

Some CSS

Upon consulting with Denes, we agreed on this thin line look, which resembles some tree. With multiple levels, the main headings should be clear along with their children.

.autoc ul{
    margin-left: 45px;
    padding-left: 0;
    border-left: 1px solid #c4cbdb;
.autoc li {
    padding-left: 20px;
    list-style: none;
    margin-bottom: 0px;
.autoc .autoc-level-H3{
    padding-left: 40px;
.autoc .autoc-level-H4{
    padding-left: 60px;
.autoc .autoc-level-H2:before{
    content: "";
    display: block;
    height: 0;
    width: 8px;
    border-bottom: 1px dashed #c4cbdb;
    /* Adjust the Y value for your site */
    transform: translateX(-15px) translateY(19px);
.autoc .autoc-level-H3:before{
    content: "";
    display: block;
    height: 0;
    width: 30px;
    border-bottom: 1px dashed #c4cbdb;
    /* Adjust the Y value for your site */
    transform: translateX(-35px) translateY(19px);
.autoc .autoc-level-H4:before{
    content: "";
    display: block;
    height: 0;
    width: 50px;
    border-bottom: 1px dashed #c4cbdb;
    /* Adjust the Y value for your site */
    transform: translateX(-55px) translateY(19px);
@media(max-width: 767px) {
    .autoc ul{
        margin-left: 0;
    .autoc .autoc-level-H2:before,
    .autoc .autoc-level-H3:before,
    .autoc .autoc-level-H4:before{
        /* Adjust the margin-top value for your site */
        margin-top: -3px;
        position: absolute;

The only challenge here was to style an otherwise unordered list nothing like our normal lists. However, you need to adjust some values for your specific situation. Those that require your attention have a comment above them; they depend on your font-size and are about the vertical position of the dashed line. We define styles for h2-h3-h4 levels only. As h1 is the title of the article and we don’t use h5-h6, and even if we did, we don’t want those in the table of contents.

Adding automatic table of contents to your site

Make yourself familiar with my methods for adding PHP code snippets to WordPress. The CSS goes into your child theme, but the native customizer also has a place for it. Hopefully, your child theme already has a custom.js file for the JS, but if not, you can create it and then load via PHP:

function custom_scripts() {
add_action('wp_enqueue_scripts', 'custom_scripts');

The solution is not yet ready to become a plugin, but we plan on extending this idea further. Let us know if it’s a useful addition to our or your site!

Did this solution improve Let's WP?

This site is powered by Elementor

  • This site is powered by Elementor

Related Posts

Comments are closed.

Check out Justified Image Grid, my top-selling WordPress gallery that shows photos without cropping!

Show your photos with Justified Image Grid!