How to create CMS dynamic table of contents (Step by step tutorial)

Base on this plugin (There are many more similar libraries):
http://projects.jga.me/toc/

Example of blog posts (CMS)

1/5:

The normal process (Bind the rich text area) for post collection page


2/5 - toc div

Add empty div with class of toc


3 - add custom code

Before body copy-paste the custom code below (The code is very small - less than 1KB (774 bytes). No cdn for this library)

<script>
 /*!
 * toc - jQuery Table of Contents Plugin
 * v0.3.2
 * http://projects.jga.me/toc/
 * copyright Greg Allen 2014
 * MIT License
*/
!function(a){a.fn.smoothScroller=function(b){b=a.extend({},a.fn.smoothScroller.defaults,b);var c=a(this);return a(b.scrollEl).animate({scrollTop:c.offset().top-a(b.scrollEl).offset().top-b.offset},b.speed,b.ease,function(){var a=c.attr("id");a.length&&(history.pushState?history.pushState(null,null,"#"+a):document.location.hash=a),c.trigger("smoothScrollerComplete")}),this},a.fn.smoothScroller.defaults={speed:400,ease:"swing",scrollEl:"body,html",offset:0},a("body").on("click","[data-smoothscroller]",function(b){b.preventDefault();var c=a(this).attr("href");0===c.indexOf("#")&&a(c).smoothScroller()})}(jQuery),function(a){var b={};a.fn.toc=function(b){var c,d=this,e=a.extend({},jQuery.fn.toc.defaults,b),f=a(e.container),g=a(e.selectors,f),h=[],i=e.activeClass,j=function(b,c){if(e.smoothScrolling&&"function"==typeof e.smoothScrolling){b.preventDefault();var f=a(b.target).attr("href");e.smoothScrolling(f,e,c)}a("li",d).removeClass(i),a(b.target).parent().addClass(i)},k=function(){c&&clearTimeout(c),c=setTimeout(function(){for(var b,c=a(window).scrollTop(),f=Number.MAX_VALUE,g=0,j=0,k=h.length;k>j;j++){var l=Math.abs(h[j]-c);f>l&&(g=j,f=l)}a("li",d).removeClass(i),b=a("li:eq("+g+")",d).addClass(i),e.onHighlight(b)},50)};return e.highlightOnScroll&&(a(window).bind("scroll",k),k()),this.each(function(){var b=a(this),c=a(e.listType);g.each(function(d,f){var g=a(f);h.push(g.offset().top-e.highlightOffset);var i=e.anchorName(d,f,e.prefix);if(f.id!==i){a("<span/>").attr("id",i).insertBefore(g)}var l=a("<a/>").text(e.headerText(d,f,g)).attr("href","#"+i).bind("click",function(c){a(window).unbind("scroll",k),j(c,function(){a(window).bind("scroll",k)}),b.trigger("selected",a(this).attr("href"))}),m=a("<li/>").addClass(e.itemClass(d,f,g,e.prefix)).append(l);c.append(m)}),b.html(c)})},jQuery.fn.toc.defaults={container:"body",listType:"<ul/>",selectors:"h1,h2,h3",smoothScrolling:function(b,c,d){a(b).smoothScroller({offset:c.scrollToOffset}).on("smoothScrollerComplete",function(){d()})},scrollToOffset:0,prefix:"toc",activeClass:"toc-active",onHighlight:function(){},highlightOnScroll:!0,highlightOffset:100,anchorName:function(c,d,e){if(d.id.length)return d.id;var f=a(d).text().replace(/[^a-z0-9]/gi," ").replace(/\s+/g,"-").toLowerCase();if(b[f]){for(var g=2;b[f+g];)g++;f=f+"-"+g}return b[f]=!0,e+"-"+f},headerText:function(a,b,c){return c.text()},itemClass:function(a,b,c,d){return d+"-"+c[0].tagName.toLowerCase()}}}(jQuery);
</script>

4/5 - Initialize

Initialize the plugin.
Jquery class selctor (“.toc”) - read her about jquery selectors:

Again keep in mind the selectors must match (In my example .toc class and .rich-text-block class)
**paste this code after the code from step 3

<script>
/* initialize toc plugin */
$('.toc').toc({
    'selectors': 'h1,h2,h3', //elements to use as headings
    'container': '.rich-text-block', //element to find all selectors in
    'prefix': 'toc', //prefix for anchor tags and class names
    'highlightOnScroll': true, //add class to heading that is currently in focus
    'highlightOffset': 100, //offset to trigger the next headline
 });
 </script>

Full copy-paste code

copy-paste (before body) install for toc div and rich-text-block class.

<script>
 /*!
 * toc - jQuery Table of Contents Plugin
 * v0.3.2
 * http://projects.jga.me/toc/
 * copyright Greg Allen 2014
 * MIT License
*/
!function(a){a.fn.smoothScroller=function(b){b=a.extend({},a.fn.smoothScroller.defaults,b);var c=a(this);return a(b.scrollEl).animate({scrollTop:c.offset().top-a(b.scrollEl).offset().top-b.offset},b.speed,b.ease,function(){var a=c.attr("id");a.length&&(history.pushState?history.pushState(null,null,"#"+a):document.location.hash=a),c.trigger("smoothScrollerComplete")}),this},a.fn.smoothScroller.defaults={speed:400,ease:"swing",scrollEl:"body,html",offset:0},a("body").on("click","[data-smoothscroller]",function(b){b.preventDefault();var c=a(this).attr("href");0===c.indexOf("#")&&a(c).smoothScroller()})}(jQuery),function(a){var b={};a.fn.toc=function(b){var c,d=this,e=a.extend({},jQuery.fn.toc.defaults,b),f=a(e.container),g=a(e.selectors,f),h=[],i=e.activeClass,j=function(b,c){if(e.smoothScrolling&&"function"==typeof e.smoothScrolling){b.preventDefault();var f=a(b.target).attr("href");e.smoothScrolling(f,e,c)}a("li",d).removeClass(i),a(b.target).parent().addClass(i)},k=function(){c&&clearTimeout(c),c=setTimeout(function(){for(var b,c=a(window).scrollTop(),f=Number.MAX_VALUE,g=0,j=0,k=h.length;k>j;j++){var l=Math.abs(h[j]-c);f>l&&(g=j,f=l)}a("li",d).removeClass(i),b=a("li:eq("+g+")",d).addClass(i),e.onHighlight(b)},50)};return e.highlightOnScroll&&(a(window).bind("scroll",k),k()),this.each(function(){var b=a(this),c=a(e.listType);g.each(function(d,f){var g=a(f);h.push(g.offset().top-e.highlightOffset);var i=e.anchorName(d,f,e.prefix);if(f.id!==i){a("<span/>").attr("id",i).insertBefore(g)}var l=a("<a/>").text(e.headerText(d,f,g)).attr("href","#"+i).bind("click",function(c){a(window).unbind("scroll",k),j(c,function(){a(window).bind("scroll",k)}),b.trigger("selected",a(this).attr("href"))}),m=a("<li/>").addClass(e.itemClass(d,f,g,e.prefix)).append(l);c.append(m)}),b.html(c)})},jQuery.fn.toc.defaults={container:"body",listType:"<ul/>",selectors:"h1,h2,h3",smoothScrolling:function(b,c,d){a(b).smoothScroller({offset:c.scrollToOffset}).on("smoothScrollerComplete",function(){d()})},scrollToOffset:0,prefix:"toc",activeClass:"toc-active",onHighlight:function(){},highlightOnScroll:!0,highlightOffset:100,anchorName:function(c,d,e){if(d.id.length)return d.id;var f=a(d).text().replace(/[^a-z0-9]/gi," ").replace(/\s+/g,"-").toLowerCase();if(b[f]){for(var g=2;b[f+g];)g++;f=f+"-"+g}return b[f]=!0,e+"-"+f},headerText:function(a,b,c){return c.text()},itemClass:function(a,b,c,d){return d+"-"+c[0].tagName.toLowerCase()}}}(jQuery);
</script>

<script>
/* initialize toc plugin */
$('.toc').toc({
    'selectors': 'h1,h2,h3', //elements to use as headings
    'container': '.rich-text-block', //element to find all selectors in
    'prefix': 'toc', //prefix for anchor tags and class names
    'highlightOnScroll': true, //add class to heading that is currently in focus
    'highlightOffset': 100, //offset to trigger the next headline
 });
 </script>

5/5 - publish

Publish the site

Click

Styles:

If you want to create “multi-level” effect (This is not really nested UL) -

Add extra margin for h3 list elements (Better UI):

<style>
.toc-h3{
  margin-left: 20px; 
}
</style>

scrollspy effect

Use .toc-active to style the current menu active (For sidebars menu with active state change on scroll)


scroll:

Related JS option: 'highlightOffset': 100, //offset to trigger the next headline

#webflow blog posts table of contents

Weaknesses: Other TOC libraries know to create nested lists (And number lists) for complex data. Advantages: Tiny code, fast, simple.

11 Likes

Thanks for this! I manage to set it up :wink:

Anyone can explain to me how/where to apply CSS for the ToC?

Thanks!
Ohyoon

This is the classes the JS added to the LI (list element).
'prefix': 'toc', //prefix for anchor tags and class names
Than the plugin “generate” this class:
toc-h2
and
toc-h3
and
toc-h4
image

So you 100% knows the class name. How to create styles. By webflow create UL list and use this class name for the li

Now webflow generate in the CSS this class

.toc-h2 {
    background-color: rgb(125, 245, 191);
    color: rgb(250, 17, 17);
    font-size: 33px;
    line-height: 40px;
    font-style: italic;
    text-decoration: line-through;
}

So when you publish the site the design change:

style active state

create combo-class for “toc-active” (For sticky sidebars).

Publish the site - and the first toc (Active) style change:

style the link inside

One small problem. No way to style this
.li a (The link inside the list item). You should manually add custom code (before head or as embed html)

<style>
.toc-h2 a, .toc-h3 a, .toc-h4 a{
   color: orange;
   font-weight: 800;
   /*more styles her */
}
/* specific class for h2 level toc */
.toc-h2 a, .toc-h3 a, .toc-h4 a{
   text-align: right;
}

</style>

Read her about descendant-selector:

2 Likes

This is great! Works like a charm.
But I have a question:
I have a page where each paragraph is a separate collection item. And in some cases the the user can hide the H2 heading with a switch. But with this TOC system these hidden H2’s will show up in de TOC. Is there a way to make it that when de h2 is hidden, it also disappears from the TOC?

So basically I would have to exclude the class: .w-condition-invisible

Thanks!

=======
EDIT: I found the solution myself. For anyone interested:
this is how i applied the code above:

    <script>
    /* initialize toc plugin */
    $('.toc').toc({
        'selectors': "h2:not('.w-condition-invisible')", //elements to use as headings
        'container': '.textpage_middlecolumn', //element to find all selectors in
        'prefix': 'toc', //prefix for anchor tags and class names
        'highlightOnScroll': true, //add class to heading that is currently in focus
        'highlightOffset': 100, //offset to trigger the next headline
     });
     </script>

So in this line:
‘selectors’: “h2:not(‘.w-condition-invisible’)”, //elements to use as headings

I selected all H2, but excluded the class: .w-condition-invisible.

1 Like

Hello and thank you for this great tutorial! I used it and it works well except one thing: I can’t set the distance between the top edge of the page and next H2 in the TOC, which I clicked before. As I understand, to do this I must change ‘highlightOffset’ parameter, right? By default it’s set to 100. And I tried to change it. But it doesn’t work =(( Even here it doesn’t work. Nothing is changing after that.

Can you please help me with this? Where I can change that offset if not here?
Thank you!

How to create a table styles like this
tks

1 Like

Toc and sticky/fixed navbar offset

For fixed navbar try this trick.
Before body - copy-paste

The selector:
.rich-text-block [id] - select all #id elements (The anchors) inside rich-text-block element.

<style>
.rich-text-block [id^="toc"] {
  display: block;
  position: relative;
  top: -100px;  /* her change the value to your navbar height*/
  visibility: hidden;
}
</style>

TOGGLE show/hide toc

The show/hide - is simple toggle - you could achieve this by webflow interactions (simple accordion effect).

Two elements - on click on hide (hide hide-btn show show-btn) and the opposite.

div (wrapper)
--inside--heading + show button (on click show toc) + hide button (on click hide toc)
--the .toc element

*** This issue not related to toc but to webflow interactions (Maybe open separate forum Q about this topic).

This works for the anchor links, but at the same time it hides all images within the same div.

I tried excluding the images by adding :not(.w-richtext figure img) but it didn’t work unfortunately.

<style>
   .l-content-block:not(.w-richtext figure img) [id] {
  	display: block;
  	position: relative;
  	top: -100px;  /* her change the value to your navbar height*/
  	visibility: hidden;
	}
</style>

Any work around?

Try this (more specific selector):

<style>
.rich-text-block [id^="toc"] {
  display: block;
  position: relative;
  top: -100px;  /* her change the value to your navbar height*/
  visibility: hidden;
}
</style>

1 Like

Awesome, that works :slight_smile: Thank you!

1 Like

Hi,

I’ve added the code(s) as directed in the steps…

Unfortunately the TOC is not generated.

Any suggestions on what I am doing wrong?

Thanks for your help,

Link to read-only page

Hey Ezra!

Thanks for great tutorial. Works like a charm.

Does any one know how to remove the underscore in the toc-h2 links generated? I tried custom code, all other styles worked but not text-decoration.

I’ve documented how to create anchor links into CMS elements. It turned out to be a lot simpler than I thought.

All you need to do is create an anchor inside a code block with . And then link text to the anchor by using #.

It took me a lot longer to figure out than I thought, so I recorded a quick video about this.

To do smooth scroll, add the following code to the body of the CMS Page:

<style>
html {
	scroll-behavior: smooth;
  }
  </style>
3 Likes

The bullets related to the list (Not to a specific list item).

Custom css (head) - select all lists (UL tag) inside .toc class node.

Most of the time without bullets you need to remove the extra padding/margin.

<style>
.toc ul{
    margin: 0;
    padding: 0;
    list-style: none;
}
</style>

Use another plugin (This idea build in - in tocify **very old JS plugin):
http://gregfranko.com/jquery.tocify.js/

Newer plugin:

Hey everyone!

If you need to dynamically generate a table of contents but don’t want to bother with the step-by-step tutorials this cloneable project is for you! :grin:

I followed this tutorial for my blog, so figured I might as well share it here. I hope it works well for you.

Kapture 2022-03-18 at 09.08.24

1 Like

Hi!

I’m new to this forum but I’ve been trying for days to create a TOC with many tutorials and I can’t seem to link the TOC to the sections of the rich text. I don’t know what else to do.

What can I be doing wrong?