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.

10 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).
image

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

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>
2 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>

https://www.w3schools.com/howto/howto_css_list_without_bullets.asp

any one here how know how to make all the h3 after h2 to display none and only when you click the h2 they show?

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