Automatically generate table of contents using jQuery

August 20, 2009

Some time ago, I was debating with my friends on the topic: is there any use of generating table of contents automatically. The conclusion was that it can be useful in cases when the reading material is long enough and table of contents (TOC) has a fixed position on the screen. This tutorial will show you how to create such TOC in just a few lines of code.

Extract information from HTML

I will start with the basic example – where we just want to get title and subtitles and show them before the article itself.

<div id="toc"></div>
    <div id="content">
    <h1>Title goes her</h1>
    <h2>Subtitle goes here</h2>
    <p>Text goes here...</p>
</div>

Div element with “toc” id will be a container for TOC links (this can be also added dynamically but for this example, we’ll keep it in static structure). The actual article is placed in div element with id “content”. Check out demo 1 for full lorem ipsum article. Now let’s create TOC. First we’ll append a paragraph inside “toc” container with text “In this article”. You might have seen this in some blog posts around. Next, we’ll find all H1, H2 and H3 elements and assign unique id (for this page) to each one of them. This will make them easily accessible on click. At the end, we’ll append a link for each heading.

$("#toc").append('<p>In this article:</p>')
$("h1, h2, h3").each(function(i) {
    var current = $(this);
    current.attr("id", "title" + i);
    $("#toc").append("<a id='link" + i + "' href='#title" +
        i + "' title='" + current.attr("tagName") + "'>" + 
        current.html() + "</a>");
});

The output of this code looks like this:

<div id="toc">
<p>In this article:</p>
<a id="link0" title="H1" href="#title0">Article title</a>
<a id="link1" title="H2" href="#title1">Header Level 2</a>
<a id="link2" title="H3" href="#title2">Header Level 3</a>
<a id="link3" title="H3" href="#title3">Header level 3 again</a>
<a id="link4" title="H3" href="#title4">Header level 3 once again</a>
</div> 

And headings will have id’s like in the example below:

<h1 id="title0">Article title</h1>

View demo 1

Fixing TOC position

In order to fix the position of TOC we have to do two things. First one is to wrap TOC and content into a div and center it on the screen (you can place TOC anywhere you like, but in this example we’ll fix it to the left of the content).

<div id="container">
    <div id="toc"></div>
    <div id="content">
        <h1>Title goes her</h1>
        <h2>Subtitle goes here</h2>
        <p>Text goes here...</p>
    </div>
</div> 

Next, set #toc to float left with fixed position and #content to float right.

#container { width:960px; overflow:hidden; margin:0px auto; position:relative;}
#content { width:660px; float:right;}
#toc { width:200px; position:fixed; float:left;}
#toc a { display:block; color:#0094FF;}

This will ensure that TOC remains on the screen on a fixed position while scrolling.

View demo 2

Scaled Table of Contents

The idea is to get positions of all headings and scale them vertically on a page. This can give users the idea of how long sections are.

View demo 3

We would have to modify the script from previous demo:

$("h1, h2, h3").each(function(i) {
    var current = $(this);
    current.attr("id", "title" + i);
    
    var pos = current.position().top / $("#content").height() * $(window).height();
    $("#toc").append("<a id='link" + i + "' href='#title" + i +
    "' title='" + current.attr("tagName") + "'>" +
    current.html() + "</a>");
    
    $("#link" + i).css("top", pos);
});

This code determines the position of each heading (current.position().top) and scale it to fit in the window. After appending HTML anchor, a new position is assigned via CSS. Finally, we have to position HTML anchors absolutely. Also, each heading type has different font size. The CSS in demo 3 looks like this:

#toc { width:200px; position:fixed; float:left; position:fixed;
    top:0px; background:transparent url(bkg.png) right; height:100%;}
#toc a { display:block; position:absolute; width:190px; text-align:right;
    background:transparent url(chapter_bullet.png) no-repeat right;
    color:#0094FF;  padding-right:10px;}
#toc a[title=H1] { font-size:18px;}
#toc a[title=H2] { font-size:14px;}
#toc a[title=H3] { font-size:10px;}

This is an interesting concept and would be even more interesting if the area between headings won’t be empty. Instead, it can show scaled preview of each paragraph. But this is a topic for other tutorial. What do you think?

Let's discuss this on twitter.

21 Comments

  • Richard Reddy (August 20, 2009)

    This is a really cool idea. Automating the little things like this can really help speed up a site build….which is something I’m always on the look out for.

    Cheers,
    Rich

  • tzs (August 20, 2009)

    Great plug-in, and my suggestion is try to mix with the jQuery anchor plug-in for the smooth scrolling:)

  • Douglas Neiner (August 20, 2009)

    Awesome walkthrough. Love the concept. I think I would use a UL/LI list for the TOC vs. straight links, but its still the same concept. Really love it.. I think I might try to turn it into a jQuery plugin. Seems pretty useful.

    Great job!

  • Steffen Jørgensen (August 20, 2009)

    A very interesting idea to scale the TOC. As you point out the idea still needs some work, because there’s too much room between each TOC-element. Maybe not using the entire height of the left coloumn to show the scaled TOC?

  • Janko (August 20, 2009)

    Thanks guys!

    Douglas: yes, it’s better to use UL for TOC, I missed that one.

    Steffen: To scale it even more.. not a bad idea at all. I’d also experiment with a paragraph preview. There is a plugin for Visual Studio that replaces the vertical scrollbar on documents and turns it into a scrollable and clickable preview of a document. Not that you can see real code in previewer but you can easily guess where your code is.

  • 9swords (August 21, 2009)

    Great concept, the comments here are also very good idea’s.

  • David Millar (August 21, 2009)

    I came up with this idea at one point as well, but my biggest frustration when implementing it was the order in which jQuery puts everything.

    In your examples, the order is fine since you have h1, h2, then h3 in your HTML and that’s the same order as the jQuery selector.

    Instead, my page had HTML more like h1, h2, h3, h3, h2, h3, h3, h3, h3, h2, h3. When I used the jQuery selector to grab everything, I instead got a TOC that looked like this: h1, h2, h2, h2, h3, h3, h3, h3, h3, h3, h3. So essentially, jQuery takes each tag in the order that you ask for it, not grabbing everything that matches in the order on the page.

  • Derek Pennycuff (August 21, 2009)

    Can I put code in here? I wrote a TOC in jQuery using nested lists a while back.
    <code>
    $(‘.tocMe h1:first’).after(‘<div id="toc"><h2>Table of Contents</h2><ul></ul></div>’);
    hLevelPrev = 1;
    hLevel = 2;
    myID = ‘h2-i0’;
    tocList = ”;
    $(‘.tocMe’).children(‘:header’).not(‘h1’).each(function(i) {
    $this = $(this);
    hTag = ‘h’ + hLevel;
    if ($this.is(hTag)) {
    //do nothing special
    } else if ($this.is(‘h’ + (hLevel + 1))) {
    hLevelPrev = hLevel;
    hLevel = hLevel + 1;
    tocList += ‘<ul>’;
    } else if ($this.is(‘h’ + (hLevel – 1))) {
    hLevelPrev = hLevel;
    hLevel = hLevel – 1;
    tocList += ‘</li></ul>’
    } else if ($this.is(‘h’ + (hLevel – 2))) {
    hLevelPrev = hLevel;
    hLevel = hLevel – 2;
    tocList += ‘</li></ul></li></ul>’

    } else if ($this.is(‘h’ + (hLevel – 3))) {
    hLevelPrev = hLevel;
    hLevel = hLevel – 3;
    tocList += ‘</li></ul></li></ul></li></ul>’

    } else if ($this.is(‘h’ + (hLevel – 4))) {
    hLevelPrev = hLevel;
    hLevel = hLevel – 4;
    tocList += ‘</li></ul></li></ul></li></ul></li></ul>’

    } else {
    return;
    }
    myID = ‘h’ + hLevel + ‘-i’ + i;
    $this.attr(‘id’,myID);
    tocList += ‘<li id="toc-‘ + myID + ‘"><a href="#’ + myID + ‘">’ + $this.html() + ‘</a>’;
    });
    tocList += ‘</li>’;
    $(‘#toc>ul’).append(tocList);
    </code>
    The logic to figure out the nesting levels was the hardest part. You can go backwards from H6 to H2, but you can only move forward one H level at a time. That’s a constraint I don’t mind having on my markup. But I never thought anyone else would ever have a use of this code so I didn’t even consider more general mark up practices.

    I pulled this out of production after testing in IE6, which was very slow. At the time it had a 20% market share in our audience. That’s now down to 10% so I might have to dust off this code some day. I’m sure there are ways to optimize it. My jQuery fu is fairly weak.

  • Marko Randjelovic (August 22, 2009)

    Great tutorial, Janko! I think that I’ll use it in one of my upcoming projects. I’ll share the link as soon as it goes live. ;)

  • Davide Espertini (August 24, 2009)

    I think this is an awesome idea.
    I can use it for a lot of projects that are in my todo list for the future!

    Cheers,
    Davide

  • Steven (August 25, 2009)

    To order the headings (from the pageContents div) in DOM order I use

    $("#pageContents h1, #pageContents h2, #pageContents h3, #pageContents h4").addClass("toc_heading");

    then I use

    $(".toc_heading").each ….

    It seems a bit ugly but does pull the headings out of the page in DOM order

  • Gerasimos (August 25, 2009)

    Yeah, great idea indeed. Using UL and anchors as some ppl mentioned would make a great plugin. Nice one :)

  • Stephen Cronin (August 26, 2009)

    Very nice. One tip though:

    Instead of adding the &lt;id="toc"&gt; and appending things to it, I’d suggest building a string of the HTML you want INCLUDING the &lt;id="toc"&gt; then use $(‘div#content).before(string); to append the string. That way you don’t need to add a non semantic empty div (ie toc) in the markup.

    It’s only a small thing, but it’s slightly more semantic code.

  • Ivan Minic (August 27, 2009)

    Love it. Especially the way demo 2 and 3 look. The idea is really nice and clean ;)

  • aravind (August 27, 2009)

    Great idea Janko! you rock!!!

  • vignesh (August 31, 2009)

    great work…

  • website laten maken (September 1, 2009)

    Wow, that last example is awesome! Never really saw something like that before, I really love it! Keep it up, you’re jQuery articles are great!

  • Marco (September 3, 2009)

    That – is – absolutely – amazing. Well done Janko, the idea is great, just as how it looks. This really could be implemented somewhere and it’s very useful. Great work mate!

  • Derek Pennycuff (October 7, 2009)

    The code I posted back in August apparently does not play well with Jquery 1.3. It double inserts the "Table of Contents" header for some reason. So I tweaked it a bit.

    $(‘#tocMe’).after(‘<div id="toc"><h2>Table of Contents</h2></div>’);
    hLevelPrev = 1;
    hLevel = 2;
    myID = ‘h2-i0’;
    tocList = ‘<ul>’;
    $(‘#tocMe ~ :header’).each(function(i) {
    $this = $(this);
    hTag = ‘h’ + hLevel;
    if ($this.is(hTag)) {
    //good job!
    } else if ($this.is(‘h’ + (hLevel + 1))) {
    hLevelPrev = hLevel;
    hLevel = hLevel + 1;
    tocList += ‘<ul>’;
    } else if ($this.is(‘h’ + (hLevel – 1))) {
    hLevelPrev = hLevel;
    hLevel = hLevel – 1;
    tocList += ‘</li></ul>’
    } else if ($this.is(‘h’ + (hLevel – 2))) {
    hLevelPrev = hLevel;
    hLevel = hLevel – 2;
    tocList += ‘</li></ul></li></ul>’

    } else if ($this.is(‘h’ + (hLevel – 3))) {
    hLevelPrev = hLevel;
    hLevel = hLevel – 3;
    tocList += ‘</li></ul></li></ul></li></ul>’

    } else if ($this.is(‘h’ + (hLevel – 4))) {
    hLevelPrev = hLevel;
    hLevel = hLevel – 4;
    tocList += ‘</li></ul></li></ul></li></ul></li></ul>’

    } else {
    return;
    }
    myID = ‘h’ + hLevel + ‘-i’ + i;
    $this.attr(‘id’,myID);
    tocList += ‘<li id="toc-‘ + myID + ‘"><a href="#’ + myID + ‘">’ + $this.html() + ‘</a>’;
    });
    tocList += ‘</li></ul>’;
    $(‘#toc>h2’).after(tocList);

  • Spaceman-Spiff (November 16, 2009)

    Nice script.

    Would be cool if you can add scrolling animation when moving to the anchors/headers.

  • Maicon Sobczak (November 17, 2009)

    Yeah! Such a beautiful solution!