Dynamic table of contents with Tocbot

Tocbot allows you to quickly and easily create dynamic table of contents. Here's how I made a plugin to quickly achieve this.

Note: The original Oxygen Tocbot tutorial is by Sridhar Katakam.


Tocbot.js is a lightweight, open source library developed by Tim Scanlin which uses vanilla JavaScript to create dynamic table of contents using native DOM methods. It is highly customisable using the prebuilt functions and supported on almost all browsers.

My plugin files


There are two requirements in the PHP file for this plugin. The first is to enqueue the Tocbot.js file (more on this below) - so the snippet will look something like this:

Plugin Name:    Tocbot
Plugin URI: https://www.farhan.app/
Description:   Tocbot plugin created for Oxygen Builder.
Version:    1.0.0
Author:		Farhan
Author URI:	https://www.farhan.app/

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

	if ( is_singular( 'post' ) ) {

			plugin_dir_url( __FILE__ ) . 'assets/tocbot.min.js',


The second part of the PHP is an addition to dynamically add ID's to headings only in blog posts. The snippet looks like this:

function auto_id_headings( $content ) {

	$content = preg_replace_callback( '/(\<h[1-6](.*?))\>(.*)(<\/h[1-6]>)/i', function( $matches ) {
		if ( ! stripos( $matches[0], 'id=' ) ) :
			$matches[0] = $matches[1] . $matches[2] . ' id="' . sanitize_title( $matches[3] ) . '">' . $matches[3] . $matches[4];
		return $matches[0];
	}, $content );

    return $content;

add_filter( 'the_content', 'auto_id_headings' );

Credit to Jeroen Sormani for providing this snippet.

Note: It is important for all headings you want to show up in your dynamic table of content using Tocbot to have ID's.


Although I chose not to enqueue the CSS in the plugin, it makes sense to do so. I simply added the following CSS into a code block in WordPress:

.toc {
    overflow-y: auto;
.toc > .toc-list {
    overflow: hidden;
    position: relative;
.toc > .toc-list li {
    list-style: none;
.toc-list {
    margin: 0;
    padding-left: 15px;
a.toc-link {
    color: currentColor;
    height: 100%;
    font-weight: 400;
    text-decoration: none;
.is-collapsible {
    max-height: 1000px;
    overflow: hidden;
    transition: all 300ms ease-in-out;
.is-collapsed {
    max-height: 0;
.is-position-fixed {
    position: fixed !important;
    top: 0;
.is-active-link {
    font-weight: 700 !important;
.toc-link::before {
    background-color: #eee;
    content: " ";
    display: inline-block;
    height: inherit;
    left: 0;
    margin-top: -12px;
    position: absolute;
    width: 2px;
.is-active-link::before {
    background-color: #000;
@media only screen and (min-width: 993px) {
    .sticky {
        position: -webkit-sticky;
        position: sticky;
        top: 0;
        align-self: flex-start;
@media only screen and (max-width: 992px) {
    .is-collapsed {
        max-height: none;
.admin-bar .sticky {
    top: 32px;
.sticky:after {
    content: "";
    display: table;
a.toc-link {
    padding: 7px 0;
    display: block;
    line-height: 1.4;
    font-size: 1.6rem;
a.toc-link:hover {
    color: #000;
.is-collapsible a.toc-link {
    padding: 5px 0;
a.toc-link {
    line-height: 1.4;

Credit to Sridhar Katakam for providing this snippet.

Note: The CSS snippet has been minified as it is quite long.


Firstly, I saved the Tocbot initialisation JavaScript file which you can find here in my assets directory of the plugin - which is then enqueued (previously shown in PHP section).

In order to use Tocbot, it must be initialised on the page of where it will be used. I simply added the following snippet in my Single Post Template (but it can also be added to the JavaScript file which is enqueued to the plugin):

  tocSelector: ".toc",
  contentSelector: ".post__main__content .ct-inner-content",
  headingSelector: "h2, h3",
  headingsOffset: 100,
  scrollSmoothOffset: -100

Where ".toc" is the class of a blank div where the actual table of contents gets generated from the template.

Where ".post__main__content" is the class of my main content div which houses the Inner Content element for the post in the template.

Note: You can get away without using the additional ".ct-innter content" portion, but I have my related posts in the ".post__main__content" div so I had to be a bit extra with my selector to make sure they are not included in my table of contents.

I also added a heading offset to activate the current table of contents item when the corresponding header item has an offset of 100px from the top of the screen and scroll offset to position the top of the anchor item 100px from the top. They do not have to be the same. This was just my preference, but you can change this as required.

That's pretty much it for the plugin.

TLDR - free plugin download

If you don't want to create the plugin yourself using the above snippets, you can download the free plugin I have put together from here. You may still need to follow the JS initialisation steps outlined in this post.

Again, massive shout-out to Sridhar Katakam for providing the original tutorial.

Want to be notified of new posts like this?

Sign up to be notified of new posts using the form below. Email addresses are used for the sole purpose of sending once-in-a-while email notifications and nothing else. I promise I don't spam!