In this WordPress tutorial, we’ll create some code to automatically number the headings in any post content. They can be ascending or descending and start at any number. It’s great for Top Ten list articles!
We’ll do most of the work in the browser using JavaScript, so it will work with page builders like Elementor, as well as the built-in Block Editor. Just make sure you’re using a custom child theme so you can edit “functions.php”.
infoThis is a great tutorial for anyone starting to learn PHP and JavaScript for WordPress.
We’re Going to Make This
Heading One
Officia non anim culpa, ut esse. Velit cillum pariatur velit.
Heading Two
Eu cillum ullamco tempor, enim eiusmod.
Heading Three
Velit ad sint. Consequat ipsum mollit eiusmod cupidatat, deserunt.
Heading Four
Laborum ullamco veniam tempor sed. Do anim Excepteur non.
Preparation
The logic we want to implement in the browser breaks down like this…
- Set a variable to hold the current “heading number”, usually to a value of “1”, but it could be anything.
- Find all elements on the page that match a query selector, e.g. “.entry-content h2”
- For each element that matches the query selector but does NOT have the “no-auto” CSS class…
- Create a span element to hold the heading number
- Set the span’s innerText property to the current heading number
- Add a CSS class to the span so we can style it
- Insert the span’s HTML into the heading element, before the text that’s already in there
- Add +1 (ascending) or -1 (descending) to the current heading number

All the back-end (PHP) code needs to do is enqueue our JavaScript and CSS assets. But… we don’t want to include our JS code on every single page-load… we only want to enqueue it when it’s needed. For the most part, we can use WordPress’ Conditional Tags like is_archive() and is_front_page(), but we can also do some neat stuff with a custom field (custom post meta).
Custom Field
Install the Advanced Custom Fields plugin, if it’s not already on your site.
If you’ve already got a field group associated with your content type (e.g. “Post”) then you can add the new field to it. Otherwise, you’ll need to add a new field group now.
Edit your field group and add a new True / False field. The Field Label can be anything you want, but the Field Name must be “_auto_numbered_headings” – we’re going to reference it in the code.
Save your field group and then edit a post where you want to auto-number the headings. Enable the new toggle switch and save the change.
Write the Code
In your custom child theme, create a new file called “wpt-numbered-headings.php”. Then create a folder called “wpt-numbered-headings” and add two empty text files to it, called “numbered-headings.js” and “numbered-headings.css”
Edit wpt-numbered-headings.php and paste the following into it
<?php /** * WP Tutorials : Auto-Numbered Headings (WPTNH) * * https://wp-tutorials.tech/refine-wordpress/auto-numbered-headings/ */ defined('WPINC') || die(); const WPTNH_HEADING_SELECTOR = '.entry-content h2'; const WPTNH_HEADING_START = 10; const WPTNH_HEADING_STEP = -1; const WPTNH_ENABLED_FIELD_NAME = '_auto_numbered_headings'; const WPTNH_USE_ON_ARCHIVES = false; const WPTNH_USE_ON_FRONT_PAGE = false; const WPTNH_USE_ON_BLOG = false; const WPTNH_USE_ON_ALL_POSTS = true; function wptnh_enqueue_scripts() { $enable_numbered_headings = false; $enable_numbered_headings |= WPTNH_USE_ON_ARCHIVES && is_archive(); $enable_numbered_headings |= WPTNH_USE_ON_FRONT_PAGE && is_front_page(); $enable_numbered_headings |= WPTNH_USE_ON_BLOG && is_home(); if (WPTNH_USE_ON_ALL_POSTS) { $enable_numbered_headings |= is_single(); } else { $enable_numbered_headings |= filter_var(get_post_meta(get_the_ID(), WPTNH_ENABLED_FIELD_NAME, true), FILTER_VALIDATE_BOOLEAN); } if ($enable_numbered_headings) { $theme_version = wp_get_theme()->get('Version'); $base_uri = get_stylesheet_directory_uri(); $handle = 'wptnh'; wp_enqueue_style( $handle, $base_uri . '/wpt-numbered-headings/numbered-headings.css', null, $theme_version ); wp_enqueue_script( $handle, $base_uri . '/wpt-numbered-headings/numbered-headings.js', null, $theme_version ); // Pass some data to the front-end in a global JavaScript variable // called wptnhData wp_localize_script( $handle, 'wptnhData', array( 'headingSelector' => WPTNH_HEADING_SELECTOR, 'numbering' => array( 'start' => WPTNH_HEADING_START, 'step' => WPTNH_HEADING_STEP, ), ) ); } } add_action('wp_enqueue_scripts', 'wptnh_enqueue_scripts');
Next, edit your child theme’s functions.php and add the following couple of lines:
// Headwall WP Tutorials Auto-numbered headings require_once dirname(__FILE__) . '/wpt-numbered-headings.php';
That’s the back-end code sorted out. Read through the comments and make sure you know what wp_localize_script() is doing – it’s the glue that links the back-end code to the JavaScript code in the browser.
To get started on the front-end stuff, edit “wpt-numbered-headings/numbered-headings.js” and paste this into it:
/** * WP Tutorials : Auto-Numbered Headings (WPTNH) * * https://wp-tutorials.tech/refine-wordpress/auto-numbered-headings/ */ document.addEventListener('DOMContentLoaded', function() { 'use strict'; // Uncomment this for diagnostics // console.log('WPT Numbered Headings : load'); if (typeof wptnhData !== 'undefined') { // Uncomment this for diagnostics // console.log(`WPT Numbered Headings : init`); // console.log(`Selector : ${wptnhData.headingSelector}`); let currentHeadingNumber = wptnhData.numbering.start; const headingElements = document.querySelectorAll(wptnhData.headingSelector).forEach(function(headingElement) { // Uncomment this for diagnostics // console.log(`Found heading : ${headingElement.innerText}`); if (headingElement.classList.contains('no-auto')) { // If a heading element has the "no-auto" class, just skip over it. } else { // Create a span element and set its text and CSS class. We're not // actually going to add this element to the DOM, we're going to use // the outerHTML property to "render" the element. const numberElement = document.createElement('span'); numberElement.innerText = currentHeadingNumber; numberElement.classList.add('heading-number'); // Insert the HTML representation of the span element to the // beginning of the heading. headingElement.innerHTML = `${numberElement.outerHTML}${headingElement.innerHTML}`; headingElement.classList.add('numbered-heading'); currentHeadingNumber += wptnhData.numbering.step; } }); } });
Finally, a bit of style for “wpt-numbered-headings/numbered-headings.css“:
/** * WP Tutorials : Auto-Numbered Headings (WPTNH) * * https://wp-tutorials.tech/refine-wordpress/auto-numbered-headings/ */ .numbered-heading { border-bottom: 1px solid black; } .heading-number { background-color: black; color: white; display: inline-block; width: 3em; text-align: center; margin-right: 0.5em; padding: 0.3em 0; }
Deploy, Test & Extend
If the code doesn’t seem to be working for you, uncomment the diagnostics lines in the JavaScript file and reload the content. When you open the Dev Tools JS Console, you should see confirmation that the script has loaded, initialised and found some headings. If it’s not finding any headings, you probably just need to change the element selector. Try setting WPTNHG_HEADING_SELECTOR
to something simpler like “h2”, at the top of the PHP file.
To change the numbering so it starts at 1 and counts upwards, change WPTNH_HEADING_START
to a value of 1 and WPTNH_HEADING_STEP
to a value of 1, like this:
const WPTNH_HEADING_START = 1; // 10; const WPTNH_HEADING_STEP = 1; // -1;
That’s it, really. A simple little module that doesn’t try and do more than it needs to do… keep it simple and elegant.
Happy auto-numbering π π
β¦β¦What about secondary headings?
I’ve had a think about your request and the JavaScript would need rewriting to support 2nd and 3rd level headings, which would mean the entire tutorial would probably need to be adjusted. I’ve already got the next tutorials planned but maybe I can revisit this one next month for you.
Thanks!!!
If I wanted to skip one of the h2 from the numbering, would there be an easy way to do that?
Sure thing. I’ve modified the tutorial for you now so you can add the “no-auto” CSS Class to any elements that should NOT be numbered. Just copy/paste the new JavaScript section into your project.
Thank you so much, much appreciated π
Hey, in the Advanced Custom Fields settings, I’ve set the field group location rules to apply only to one post on my website. But it’s applying to every post on my website that has an h2. Why would this be happening β and how can I fix it?
Hi again
The ACF field is only used when WPTNH_USE_ON_ALL_POSTS is set to false. Did you change this at the top of your PHP file, like this?
const WPTNH_USE_ON_ARCHIVES = false;
const WPTNH_USE_ON_FRONT_PAGE = false;
const WPTNH_USE_ON_BLOG = false;
const WPTNH_USE_ON_ALL_POSTS = false; //true; <<< Set this to false to use the ACF field