Auto-Numbered Headings

Numbered headings tutorial for WordPress

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


The logic we want to implement in the browser breaks down like this…

  1. Set a variable to hold the current “heading number”, usually to a value of “1”, but it could be anything.
  2. Find all elements on the page that match a query selector, e.g. “.entry-content h2”
  3. For each element that matches the query selector but does NOT have the “no-auto” CSS class…
    1. Create a span element to hold the heading number
    2. Set the span’s innerText property to the current heading number
    3. Add a CSS class to the span so we can style it
    4. Insert the span’s HTML into the heading element, before the text that’s already in there
    5. Add +1 (ascending) or -1 (descending) to the current heading number
the "no-auto" CSS class
Add the “no-auto” CSS class to exclude headings

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.

Enable auto-numbered headings
Enable auto-numbering at post-level
Auto numbered headings ACF field
Create an ACF field to enable numbered headings

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


 * WP Tutorials : Auto-Numbered Headings (WPTNH)
 * Changelog
 * 2024-01-11 : In the JS, if wptnhData.numbering.step is -1 (i.e. counting down)
 *              and wptnhData.numbering.start <= 0 (i.e. invalid) then start
 *              counting down from the total number of matched heading
 *              elements that are on the page.

defined('WPINC') || die();

const WPTNH_HEADING_SELECTOR = '.entry-content h2';

 * When counting downwards, set WPTNH_HEADING_START to 0 to count down
 * from the number of matched heading elements, instead of a fixed number.
const WPTNH_HEADING_START = 10; // 0;

 * Set to -1 to count downwards, or set to 1 to count up.

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();

		$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';

			$base_uri . '/wpt-numbered-headings/numbered-headings.css',

			$base_uri . '/wpt-numbered-headings/numbered-headings.js',

		// Pass some data to the front-end in a global JavaScript variable
		// called wptnhData
				'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)
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}`);

		// Find all the heading elements that match our selector.
		const headingElements = document.querySelectorAll(wptnhData.headingSelector);

		// If we're counting down, but the first heading number is <= 0 (invalid)
		// then start counting down from the number of heading elements we've
		// found.
		if (wptnhData.numbering.step < 0 && wptnhData.numbering.start <= 0) {
			wptnhData.numbering.start = headingElements.length;

		let currentHeadingNumber = wptnhData.numbering.start;

		headingElements.forEach(function(headingElement) {
			// Uncomment this for diagnostics
			// console.log(`Found heading : ${headingElement.innerText}`);

			// 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;

			// Insert the HTML representation of the span element to the
			// beginning of the heading.
			headingElement.innerHTML = `${numberElement.outerHTML}${headingElement.innerHTML}`;

			currentHeadingNumber += wptnhData.numbering.step;

Finally, a bit of style for “wpt-numbered-headings/numbered-headings.css“:

 * WP Tutorials : Auto-Numbered Headings (WPTNH)

.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 😎 πŸ‘

Like This Tutorial?

Let us know

WordPress plugins for developers

12 thoughts on “Auto-Numbered 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.

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

  2. Hi headwall,

    This script is great. I’m trying to add it to my site but how to make
    a dynamic value?

    Not all the posts on my site have the exact same count of 10 h2s.

    I know I have the option as you suggested to count upwards
    const WPTNH_HEADING_START = 1; // 10;
    const WPTNH_HEADING_STEP = 1; // -1;

    but I still want it to count downwards…

    Any suggestion will be greatly appreciated!

    • I’m glad you like it – it’s a neat little bit of code.

      I’ve made a couple of tweaks for you so the start number of the headings can be dynamic when counting downwards. You need to grab the new PHP and JS files, then set WPTNH_HEADING_START=0 and WPTNH_HEADING_STEP=-1 in the PHP file. That should do the job for you. You can see in the new JS file where we do a quick check to see if the start number is invalid, then we set it to headingElements.length.

      I hope this helps.

      • Thank you so much, Paul! I will update the code on my site and can’t wait to see how it looks!

        What you contributed to the WP community is the vivid example of the WP spirit: opensource, sharing, and caring. At least, it used to be like that…

        Again, much appreciated and a big hug from the US.


Leave a comment