Tag: Contents

A Perfect Table of Contents With HTML + CSS

Earlier this year, I self-published an ebook called Understanding JavaScript Promises (free for download). Even though I didn’t have any intention of turning it into a print book, enough people reached out inquiring about a print version that I decided to self-publish that as well .I thought it would be an easy exercise using HTML and CSS to generate a PDF and then send it off to the printer. What I didn’t realize was that I didn’t have an answer to an important part of a print book: the table of contents.

The makeup of a table of contents

At its core, a table of contents is fairly simple. Each line represents a part of a book or webpage and indicates where you can find that content. Typically, the lines contain three parts:

  1. The title of the chapter or section
  2. Leaders (i.e. those dots, dashes, or lines) that visually connect the title to the page number
  3. The page number

A table of contents is easy to generate inside of word processing tools like Microsoft Word or Google Docs, but because my content was in Markdown and then transformed into HTML, that wasn’t a good option for me. I wanted something automated that would work with HTML to generate the table of contents in a format that was suitable for print. I also wanted each line to be a link so it could be used in webpages and PDFs to navigate around the document. I also wanted dot leaders between the title and page number.

And so I began researching.

I came across two excellent blog posts on creating a table of contents with HTML and CSS. The first was “Build a Table of Contents from your HTML” by Julie Blanc. Julie worked on PagedJS, a polyfill for missing paged media features in web browsers that properly formats documents for print. I started with Julie’s example, but found that it didn’t quite work for me. Next, I found Christoph Grabo’s “Responsive TOC leader lines with CSS” post, which introduced the concept of using CSS Grid (as opposed to Julie’s float-based approach) to make alignment easier. Once again, though, his approach wasn’t quite right for my purposes.

After reading these two posts, though, I felt I had a good enough understanding of the layout issues to embark on my own. I used pieces from both blog posts as well as adding some new HTML and CSS concepts into the approach to come up with a result I’m happy with.

Choosing the correct markup

When deciding on the correct markup for a table of contents, I thought primarily about the correct semantics. Fundamentally, a table of contents is about a title (chapter or subsection) being tied to a page number, almost like a key-value pair. That led me to two options:

  • One option is to use a table (<table>) with one column for the title and one column for the page.
  • Then there’s the often unused and forgotten definition list (<dl>) element. It also acts as a key-value map. So, once again, the relationship between the title and the page number would be obvious.

Either of these seemed like good options until I realized that they really only work for single-level tables of contents, namely, only if I wanted to have a table of contents with just chapter names. If I wanted to show subsections in the table of contents, though, I didn’t have any good options. Table elements aren’t great for hierarchical data, and while definition lists can technically be nested, the semantics didn’t seem correct. So, I went back to the drawing board.

I decided to build off of Julie’s approach and use a list; however, I opted for an ordered list (<ol>) instead of an unordered list (<ul>). I think an ordered list is more appropriate in this case. A table of contents represents a list of chapters and subheadings in the order in which they appear in the content. The order matters and shouldn’t get lost in the markup.

Unfortunately, using an ordered list means losing the semantic relationship between the title and the page number, so my next step was to re-establish that relationship within each list item. The easiest way to solve this is to simply insert the word “page” before the page number. That way, the relationship of the number relative to the text is clear, even without any other visual distinction.

Here’s a simple HTML skeleton that formed the basis of my markup:

<ol class="toc-list">   <li>     <a href="#link_to_heading">       <span class="title">Chapter or subsection title</span>       <span class="page">Page 1</span>     </a>      <ol>       <!-- subsection items -->     </ol>   </li> </ol>

Applying styles to the table of contents

Once I had established the markup I planned to use, the next step was to apply some styles.

First, I removed the autogenerated numbers. You can choose to keep the autogenerated numbers in your own project if you’d like, but it’s common for books to have unnumbered forewords and afterwords included in the list of chapters, which makes the autogenerated numbers incorrect.

For my purpose, I would fill in the chapter numbers manually then adjust the layout so the top-level list doesn’t have any padding (thus aligning it with paragraphs) and each embedded list is indented by two spaces. I chose to use a 2ch padding value because I still wasn’t quite sure which font I would use. The ch length unit allows the padding to be relative to the width of a character — no matter what font is used — rather than an absolute pixel size that could wind up looking inconsistent.

Here’s the CSS I ended up with:

.toc-list, .toc-list ol {   list-style-type: none; }  .toc-list {   padding: 0; }  .toc-list ol {   padding-inline-start: 2ch; }

Sara Soueidan pointed out to me that WebKit browsers remove list semantics when list-style-type is none, so I needed to add role="list" into the HTML to preserve it:

<ol class="toc-list" role="list">   <li>     <a href="#link_to_heading">       <span class="title">Chapter or subsection title</span>       <span class="page">Page 1</span>     </a>      <ol role="list">       <!-- subsection items -->     </ol>   </li> </ol>

Styling the title and page number

With the list styled to my liking, it was time to move on to styling an individual list item. For each item in the table of contents, the title and page number must be on the same line, with the title to the left and the page number aligned to the right.

You might be thinking, “No problem, that’s what flexbox is for!” You aren’t wrong! Flexbox can indeed achieve the correct title-page alignment. But there are some tricky alignment issues when the leaders are added, so I instead opted to go with Christoph’s approach using a grid, which as a bonus as it also helps with multiline titles. Here is the CSS for an individual item:

.toc-list li > a {   text-decoration: none;   display: grid;   grid-template-columns: auto max-content;   align-items: end; }  .toc-list li > a > .page {   text-align: right; }

The grid has two columns, the first of which is auto-sized to fill up the entire width of the container, minus the second column, which is sized to max-content. The page number is aligned to the right, as is traditional in a table of contents.

The only other change I made at this point was to hide the “Page” text. This is helpful for screen readers but unnecessary visually, so I used a traditional visually-hidden class to hide it from view:

.visually-hidden {   clip: rect(0 0 0 0);   clip-path: inset(100%);   height: 1px;   overflow: hidden;   position: absolute;   width: 1px;   white-space: nowrap; }

And, of course, the HTML needs to be updated to use that class:

<ol class="toc-list" role="list">   <li>     <a href="#link_to_heading">       <span class="title">Chapter or subsection title</span>       <span class="page"><span class="visually-hidden">Page</span> 1</span>     </a>      <ol role="list">       <!-- subsection items -->     </ol>   </li> </ol>

With this foundation in place, I moved on to address the leaders between the title and the page.

Creating dot leaders

Leaders are so common in print media that you might be wondering, why doesn’t CSS already support that? The answer is: it does. Well, kind of.

There is actually a leader() function defined in the CSS Generated Content for Paged Media specification. However, as with much of the paged media specifications, this function isn’t implemented in any browsers, therefore excluding it as an option (at least at the time I’m writing this). It’s not even listed on caniuse.com, presumably because no one has implemented it and there are no plans or signals that they will.

Fortunately, both Julie and Christoph already addressed this problem in their respective posts. To insert the dot leaders, they both used a ::after pseudo-element with its content property set to a very long string of dots, like this:

.toc-list li > a > .title {   position: relative;   overflow: hidden; }  .toc-list li > a .title::after {   position: absolute;   padding-left: .25ch;   content: " . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . ";   text-align: right; }

The ::after pseudo-element is set to an absolute position to take it out of the flow of the page and avoid wrapping to other lines. The text is aligned to the right because we want the last dots of each line flush to the number at the end of the line. (More on the complexities of this later.) The .title element is set to have a relative position so the ::after pseudo-element doesn’t break out of its box. Meanwhile, the overflow is hidden so all those extra dots invisible. The result is a pretty table of contents with dot leaders.

However, there’s something else that needs consideration.

Sara also pointed out to me that all of those dots count as text to screen readers. So what do you hear? “Introduction dot dot dot dot…” until all of the dots are announced. That’s an awful experience for screen reader users.

The solution is to insert an additional element with aria-hidden set to true and then use that element to insert the dots. So the HTML becomes:

<ol class="toc-list" role="list">   <li>     <a href="#link_to_heading">       <span class="title">Chapter or subsection title<span class="leaders" area-hidden="true"></span></span>       <span class="page"><span class="visually-hidden">Page</span> 1</span>     </a>      <ol role="list">       <!-- subsection items -->     </ol>   </li> </ol>

And the CSS becomes:

.toc-list li > a > .title {   position: relative;   overflow: hidden; }  .toc-list li > a .leaders::after {   position: absolute;   padding-left: .25ch;   content: " . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . "       ". . . . . . . . . . . . . . . . . . . . . . . ";   text-align: right; }

Now screen readers will ignore the dots and spare users the frustration of listening to multiple dots being announced.

Finishing touches

At this point, the table of contents component looks pretty good, but it could use some minor detail work. To start, most books visually offset chapter titles from subsection titles, so I made the top-level items bold and introduced a margin to separate subsections from the chapters that followed:

.toc-list > li > a {   font-weight: bold;   margin-block-start: 1em; }

Next, I wanted to clean up the alignment of the page numbers. Everything looked okay when I was using a fixed-width font, but for variable-width fonts, the leader dots could end up forming a zigzag pattern as they adjust to the width of a page number. For instance, any page number with a 1 would be narrower than others, resulting in leader dots that are misaligned with the dots on previous or following lines.

Misaligned numbers and dots in a table of contents.

To fix this problem, I set font-variant-numeric to tabular-nums so all numbers are treated with the same width. By also setting the minimum width to 2ch, I ensured that all numbers with one or two digits are perfectly aligned. (You may want to set this to 3ch if your project has more than 100 pages.) Here is the final CSS for the page number:

.toc-list li > a > .page {   min-width: 2ch;   font-variant-numeric: tabular-nums;   text-align: right; }
Aligned leader dots in a table of contents.

And with that, the table of contents is complete!

Conclusion

Creating a table of contents with nothing but HTML and CSS was more of a challenge than I expected, but I’m very happy with the result. Not only is this approach flexible enough to accommodate chapters and subsections, but it handles sub-subsections nicely without updating the CSS. The overall approach works on web pages where you want to link to the various locations of content, as well as PDFs where you want the table of contents to link to different pages. And of course, it also looks great in print if you’re ever inclined to use it in a brochure or book.

I’d like to thank Julie Blanc and Christoph Grabo for their excellent blog posts on creating a table of contents, as both of those were invaluable when I was getting started. I’d also like to thank Sara Soueidan for her accessibility feedback as I worked on this project.


A Perfect Table of Contents With HTML + CSS originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , ,

4 Quality Options for a Table of Contents Block in WordPress

Offering a table of contents block in WordPress for blog posts (or really any other type of long-ish written content) is a good idea for two reasons:

  • It helps users jump around in the post for what they need (and hopefully doesn’t get in the way).
  • It’s provides SEO value.

The RankMath SEO plugin factors it in as part of your page score (suggesting you should have one), because of that second point. See what Google likely gives you if you do it right:

Screenshoot of a typical Google search results item with a breadcrumb above the page title, then a page description. A purple box is drawn around four links below the page description to call out how adding a table of contents block in WordPress can add those links in the search results for additional user convenience,

It makes sense that other WordPress SEO Plugins like Yoast offer a table of contents block as a baked-in additional feature of the plugin. If you’re committed to using Yoast, then I think it’s fine to just use that. But I admit it’s not my favorite to feel locked to a plugin because it offers a microfeature that you then depend on.

So what are the options?

Table of Contents

What to look for in a WordPress Table of Contents Block

Here are some things to look for and think about when choosing a table of contents block:

  • Customizable header — Many options chuck a “Table of Contents” header above the actual Table of Contents, which makes sense. Can you turn it off or customize it? What level header is it? Having the ability to disable the heading might be necessary for layout, and having a choice of heading levels can help ensure proper HTML semantics.
  • Collapsible — In the spirit of making the Table of Contents less annoying, many offer a feature to toggle the Table of Contents block between open and closed states. Do you want that? Are you OK with the fact it likely requires some JavaScript to work? Is it doing toggling accessibly? Can it default to the state that you want?
  • Choose which headings to include — Perhaps you only want all the <h2> elements to form the Table of Contents. Can you do that? Do sub-headers create a “nested’ list? Do you want that? Can you turn off certain levels of headers? Can you tell the block to only include <h2> through <h4>? Are there things other than headers you want to be part of the Table of Contents?
  • Editable links — Many Table of Contents plugins in WordPress typically grab all the headings verbatim. Maybe you want to shorten, lengthen, or otherwise change a specific link in the Table of Contents; as in, not have it be the exact text of the header it links to. Can you do that?
  • Include additional links — Perhaps you want to link to something that isn’t a content heading. Perhaps it’s added to the template with a WordPress custom field, or it’s part of the overall template like the comments section. Can you add (or remove) those as headings in the block?
  • Block Editor support — That’s kind of the point of this blog post. I didn’t include many options that don’t have a block. But surely there are old school versions of this that are [shortcode]-based or that implement it some other way. I’m mostly concerned about blocks, although I could easily see a situation where your goal is to put the Table of Contents elsewhere in a template (sidebar, perhaps?). So, having multiple options and modularity could be useful.
  • Styling options — Personally, I like to bring my own styling (surprise!) and even dequeue any stylesheets (or scripts) that a plugin tries to bring along for the ride. But I can imagine more folks want the Table of Contents to look good and be able to aesthetically control it right from the editor. This means it’d be nice to have block options for colors, fonts, spacing, etc.
  • Semantic markup — Might be worth a peek at the HTML that the Table of Contents block you choose generates to make if it’s sensible. I’ve seen plugins generate HTML lists that don’t actually nest lists, for example, but instead add classes to list items to make them look nested. No, thanks. I’m not sure there is an official HTML format that’s best for SEO, so sematic markup is about the best you can do.
  • Heading IDs — In order for a linked Table of Contents to work, all the headers need to have IDs so there’s something to anchor to. I would think any plugin here that’s worth its salt would add them to headings only in the case that they don’t already have one, but you might wanna veryify that; otherwise, you run the risk of breaking existing links or perhaps even styling and functionality. Also think about what IDs are being generated. For example, I use the Add Anchor Links plugin, which adds a link (🔗) icon beside all headings to offer access to the IDs. The IDs it generates were idential to the Table of Contents-generated IDs, causing a duplicate ID problem. Fixable, but just be aware of things like that.

Option 1: Use a Dedicated Table of Contents Plugin

A dedicated Table of Contents plugin is a plugin that focuses on nothing but a Table of Contents. Nothing else. Here are some solid options where that’s the case.

Heroic Table of Contents

The ability to edit/add/remove headers from the table of contents — even after it’s been automatically generated — is pretty powerful and unique to this plugin. It allows you to open and close it (optionally) as well, which is nice as a goal for these, as they should make the links useful rather than content that gets in the way. But beware that this puts you in the territory of enqueuing additional scripts as well as styles which may or may not be ideal or something you’re comfortable doing.

Screenshot of Heroic Table of Contents Block in WordPress
(Recommended by Deborah Edwards-Onoro)

Easy Table of Contents

This is not a Block Editor block! Instead, it only automatically inserts itself, either by content type or through an opt-in checkbox in a metabox.

I find it a little awkward that you can’t control where the Table of Contents goes with this plugin. Looks like it inserts itself near the top of posts, likely right after where the <!-- more --> is located.

Screenshot of Easy Table of Contents Block in WordPress

SimpleTOC

This is my favorite one.

I like this one because it doesn’t add any scripts or styles by default. It just makes a semantic HTML list out of the headers, links them up, and that’s it. That’s how I like to roll.

Screenshot of SimpleTOC Table of Contents Block in WordPress

LuckyWP Table of Contents

Lots of features, but I find it a bit awkward how it doesn’t have regular block controls. Instead, you get this entirely custom UI for changing the settings — and you can’t preview what it looks like in the block itself.

Screenshot of LuckyWP Table of Contents Block in WordPress

GutenTOC

Feels like this Table of Contents plugin embraces the spirit of the WordPress Block Editor quite well, but I find the settings a little awkward. The choices it offers don’t feel terrifically useful (like square bullets for the list? “15” space left?).

Screenshot of the GutenTOC Table of Contents plugin in WordPress.

Option 2: Wait for a Table of Contents feature to be baked into Core WordPress

As I write, there is an open pull request to enable a Table of Contents block in the Gutenberg plugin. Presumably, should that go well, it ultimately makes its way to core. That would be great if you ask me, but it doesn’t help solve the problem of needing a Table of Contents block right this second.

If this feature does drop, I’d lean heavily toward using it. Hopefully, I can do a search or query to find existing Table of Contents blocks on all posts, switch them over to use the native block, and remove whatever plugin I have in place.

Option 3: Use a Table of Contents Block that’s a sub-feature of another WordPress plugin

I would recommend against using a plugin that does a whole slew of things just because you want to use some small part of it. But hey, if it turns out you could use lots of things from the big plugin, it could be a bonus as far as managing fewer plugins overall.

Yoast SEO Premium

The free version of the Yoast SEO plugin doesn’t have it, but for only $ 99 per year, the Yoast SEO Premium plugin does. It has almost no features at all. You just add it as a block, and it pops in. You can edit the title or remove it — it’s almost like a “sub block.”

The list of links isn’t editable, but it does update in real-time as you change headings in the content, which is something most of the others I tested didn’t do. Super basic, no styling or features, but I kinda like that. I wouldn’t run Yoast for this one feature, especially for a paid premium, but if you’re using Yoast anyway (for the long haul), then you might as well go this route.

Screenshot of the Yoast SEO plugin’s table of contents block in WordPress.

Ultimate Addons for Gutenberg

This one is probably the classiest Table of Contents block I’ve come across. Again, I’m weary of using an all-in-one plugin for one specific feature, but the other features that are baked into this plugin are things you can use, then it’s a solid option.

Screenshot of the Ultimate Addons for Gutenberg Tablew of Contents plugin in WordPress.

Option 4: Roll your own DIY Table of Contents Block

Making blocks yourself isn’t out of the question! I’ve done it a few times with create-guten-block, though I’d probably reach for @wordpress/create-block these days. This puts you in JavaScript-land, so you’ll be parsing the content of the post with JavaScript, finding headings in the post content, and building things out from there. Kind of intermediate-to-advanced territory, I’d say. On one hand, it’s extra technical debt, and on the other, at least you have complete control since it’s your own code.

Since we’re focusing on building blocks, Advanced Custom Fields has a very powerful way of building custom blocks that brings that power back to PHP-land. That way, if you’re only concerned with building a Table of Contents from other heading blocks, the code gets a lot easier.

Bill Erickson has a post — “Access block data with PHP using parse_blocks() and render_block() — that ultimately gets into literally building a Table of Contents block. This gist he provides is a pretty useful reference for how to loop through blocks on a post and produce an HTML list.

Favorite?

If I was using Yoast SEO Premium on a site, I’d just use that one. If not, I’d go for SimpleTOC. That’s what we’ve done here on CSS-Tricks. Once the core feature drops (🤞), I’d make a rainy day project of moving all posts that currently use the Table of Contents plugin over to using the core WordPress block (assuming it turns out nice).


4 Quality Options for a Table of Contents Block in WordPress originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , , ,
[Top]

Table of Contents with IntersectionObserver

If you have a table of contents on a long-scrolling page, thanks to, say, position: fixed; or position: sticky;, the IntersectionObserver API in JavaScript is the perfect companion to highlight items in the table of contents when corresponding content is in view.

Ben Frain has a post all about this:

Thanks to IntersectionObserver we have a small but very efficient bit of code to create our table of contents, provide quick links to jump around the document and update readers on where they are in a document as they read.

Compared to older techniques that need to bind to scroll events and perform their own math, this code is shorter, faster, and more logical. If you’re looking for the demo on Ben’s site, the article is the demo. And here’s a video on it:

I’ve mentioned this stuff before, but here’s a Bramus Van Damme version:

And here’s a version from Hakim el Hattab that is just begging for someone to port it to IntersectionObserver because the UI is so cool:

Direct Link to ArticlePermalink


The post Table of Contents with IntersectionObserver appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Parsing Markdown into an Automated Table of Contents

A table of contents is a list of links that allows you to quickly jump to specific sections of content on the same page. It benefits long-form content because it shows the user a handy overview of what content there is with a convenient way to get there.

This tutorial will show you how to parse long Markdown text to HTML and then generate a list of links from the headings. After that, we will make use of the Intersection Observer API to find out which section is currently active, add a scrolling animation when a link is clicked, and finally, learn how Vue’s <transition-group> allow us to create a nice animated list depending on which section is currently active.

Parsing Markdown

On the web, text content is often delivered in the form of Markdown. If you haven’t used it, there are lots of reasons why Markdown is an excellent choice for text content. We are going to use a markdown parser called marked, but any other parser is also good. 

We will fetch our content from a Markdown file on GitHub. After we loaded our Markdown file, all we need to do is call the marked(<markdown>, <options>) function to parse the Markdown to HTML.

async function fetchAndParseMarkdown() {   const url = 'https://gist.githubusercontent.com/lisilinhart/e9dcf5298adff7c2c2a4da9ce2a3db3f/raw/2f1a0d47eba64756c22460b5d2919d45d8118d42/red_panda.md'   const response = await fetch(url)   const data = await response.text()   const htmlFromMarkdown = marked(data, { sanitize: true });   return htmlFromMarkdown }

After we fetch and parse our data, we will pass the parsed HTML to our DOM by replacing the content with innerHTML.

async function init() {   const $ main = document.querySelector('#app');   const htmlContent = await fetchAndParseMarkdown();   $ main.innerHTML = htmlContent } 
 init();

Now that we’ve generated the HTML, we need to transform our headings into a clickable list of links. To find the headings, we will use the DOM function querySelectorAll('h1, h2'), which selects all <h1> and <h2> elements within our markdown container. Then we’ll run through the headings and extract the information we need: the text inside the tags, the depth (which is 1 or 2), and the element ID we can use to link to each respective heading.

function generateLinkMarkup($ contentElement) {   const headings = [...$ contentElement.querySelectorAll('h1, h2')]   const parsedHeadings = headings.map(heading => {     return {       title: heading.innerText,       depth: heading.nodeName.replace(/D/g,''),       id: heading.getAttribute('id')     }   })   console.log(parsedHeadings) }

This snippet results in an array of elements that looks like this:

[   {title: "The Red Panda", depth: "1", id: "the-red-panda"},   {title: "About", depth: "2", id: "about"},   // ...  ]

After getting the information we need from the heading elements, we can use ES6 template literals to generate the HTML elements we need for the table of contents.

First, we loop through all the headings and create <li> elements. If we’re working with an <h2> with depth: 2, we will add an additional padding class, .pl-4, to indent them. That way, we can display <h2> elements as indented subheadings within the list of links.

Finally, we join the array of <li> snippets and wrap it inside a <ul> element.

function generateLinkMarkup($ contentElement) {   // ...   const htmlMarkup = parsedHeadings.map(h => `   <li class="$ {h.depth > 1 ? 'pl-4' : ''}">     <a href="#$ {h.id}">$ {h.title}</a>   </li>   `)   const finalMarkup = `<ul>$ {htmlMarkup.join('')}</ul>`   return finalMarkup }

That’s all we need to generate our link list. Now, we will add the generated HTML to the DOM.

async function init() {   const $ main = document.querySelector('#content');   const $ aside = document.querySelector('#aside');   const htmlContent = await fetchAndParseMarkdown();   $ main.innerHTML = htmlContent   const linkHtml = generateLinkMarkup($ main);   $ aside.innerHTML = linkHtml         }

Adding an Intersection Observer

Next, we need to find out which part of the content we’re currently reading. Intersection Observers are the perfect choice for this. MDN defines Intersection Observer as follows:

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document’s viewport.

So, basically, they allow us to observe the intersection of an element with the viewport or one of its parent’s elements. To create one, we can call a new IntersectionObserver(), which creates a new observer instance. Whenever we create a new observer, we need to pass it a callback function that is called when the observer has observed an intersection of an element. Travis Almand has a thorough explanation of the Intersection Observer you can read, but what we need for now is a callback function as the first parameter and an options object as the second parameter.

function createObserver() {   const options = {     rootMargin: "0px 0px -200px 0px",     threshold: 1   }   const callback = () => { console.log("observed something") }   return new IntersectionObserver(callback, options) }

The observer is created, but nothing is being observed at the moment. We will need to observe the heading elements in our Markdown, so let’s loop over them and add them to the observer with the observe() function.

const observer = createObserver() $ headings.map(heading => observer.observe(heading))

Since we want to update our list of links, we will pass it to the observer function as a $ links parameter, because we don’t want to re-read the DOM on every update for performance reasons. In the handleObserver function, we find out whether a heading is intersecting with the viewport, then obtain its id and pass it to a function called updateLinks which handles updating the class of the links in our table of contents.

function handleObserver(entries, observer, $ links) {   entries.forEach((entry)=> {     const { target, isIntersecting, intersectionRatio } = entry     if (isIntersecting && intersectionRatio >= 1) {       const visibleId = `#$ {target.getAttribute('id')}`       updateLinks(visibleId, $ links)     }   }) }

Let’s write the function to update the list of links. We need to loop through all links, remove the .is-active class if it exists, and add it only to the element that’s actually active.

function updateLinks(visibleId, $ links) {   $ links.map(link => {     let href = link.getAttribute('href')     link.classList.remove('is-active')     if(href === visibleId) link.classList.add('is-active')   }) }

The end of our init() function creates an observer, observes all the headings, and updates the links list so the active link is highlights when the observer notices a change.

async function init() {   // Parsing Markdown   const $ aside = document.querySelector('#aside'); 
   // Generating a list of heading links   const $ headings = [...$ main.querySelectorAll('h1, h2')]; 
   // Adding an Intersection Observer   const $ links = [...$ aside.querySelectorAll('a')]   const observer = createObserver($ links)   $ headings.map(heading => observer.observe(heading)) }

Scroll to section animation

The next part is to create a scrolling animation so that, when a link in the table of contents is clicked, the user is scrolled to the heading position rather abruptly jumping there. This is often called smooth scrolling.

Scrolling animations can be harmful if a user prefers reduced motion, so we should only animate this scrolling behavior if the user hasn’t specified otherwise. With window.matchMedia('(prefers-reduced-motion)'), we can read the user preference and adapt our animation accordingly. That means we need a click event listener on each link. Since we need to scroll to the headings, we will also pass our list of $ headings and the motionQuery

const motionQuery = window.matchMedia('(prefers-reduced-motion)'); 
 $ links.map(link => {   link.addEventListener("click",      (evt) => handleLinkClick(evt, $ headings, motionQuery)   ) })

Let’s write our handleLinkClick function, which is called whenever a link is clicked. First, we need to prevent the default behavior of links, which would be to jump directly to the section. Then we’ll read the href attribute of the clicked link and find the heading with the corresponding id attribute. With a tabindex value of -1 and focus(), we can focus our heading to make the users aware of where they jumped to. Finally, we add the scrolling animation by calling scroll() on our window. 

Here is where our motionQuery comes in. If the user prefers reduced motion, the behavior will be instant; otherwise, it will be smooth. The top option adds a bit of scroll margin to the top of the headings to prevent them from sticking to the very top of the window.

function handleLinkClick(evt, $ headings, motionQuery) {   evt.preventDefault()   let id = evt.target.getAttribute("href").replace('#', '')   let section = $ headings.find(heading => heading.getAttribute('id') === id)   section.setAttribute('tabindex', -1)   section.focus() 
   window.scroll({     behavior: motionQuery.matches ? 'instant' : 'smooth',     top: section.offsetTop - 20   }) }

For the last part, we will make use of Vue’s <transition-group>, which is very useful for list transitions. Here is Sarah Drasner’s excellent intro to Vue transitions if you’ve never worked with them before. They are especially great because they provide us with animation lifecycle hooks with easy access to CSS animations.

Vue automatically attaches CSS classes for us when an element is added (v-enter) or removed (v-leave) from a list, and also with classes for when the animation is active (v-enter-active and v-leave-active). This is perfect for our case because we can vary the animation when subheadings are added or removed from our list. To use them, we will need wrap our <li> elements in our table of contents with an <transition-group> element. The name attribute of the <transition-group> defines how the CSS animations will be called, the tag attribute should be our parent <ul> element.

<transition-group name="list" tag="ul">   <li v-for="(item, index) in activeHeadings" v-bind:key="item.id">     <a :href="item.id">       {{ item.text }}     </a>   </li> </transition-group>

Now we need to add the actual CSS transitions. Whenever an element is entering or leaving it, should animate from not visible (opacity: 0) and moved a bit to the bottom (transform: translateY(10px)).

.list-enter, .list-leave-to {   opacity: 0;   transform: translateY(10px); }

Then we define what CSS property we want to animate. For performance reasons, we only want to animate the transform and the opacity properties. CSS allows us to chain the transitions with different timings: the transform should take 0.8 seconds and the fading only 0.4s.

.list-leave-active, .list-move {   transition: transform 0.8s, opacity 0.4s; }

Then we want to add a bit of a delay when a new element is added, so the subheadings fade in after the parent heading moved up or down. We can make use of the v-enter-active hook to do that:

.list-enter-active {    transition: transform 0.8s ease 0.4s, opacity 0.4s ease 0.4s; }

Finally, we can add absolute positioning to the elements that are leaving to avoid sudden jumps when the other elements are animating:

.list-leave-active {   position: absolute; }

Since the scrolling interaction is fading elements out and in, it’s advisable to debounce the scrolling interaction in case someone is scrolling very quickly. By debouncing the interaction we can avoid unfinished animations overlapping other animations. You can either write your own debouncing function or simply use the lodash debounce function. For our example the simplest way to avoid unfinished animation updates is to wrap the Intersection Observer callback function with a debounce function and pass the debounced function to the observer.

const debouncedFunction = _.debounce(this.handleObserver) this.observer = new IntersectionObserver(debouncedFunction,options)

Here’s the final demo


Again, a table of contents is a great addition to any long-form content. It helps make clear what content is covered and provides quick access to specific content. Using the Intersection Observer and Vue’s list animations on top of it can help to make a table of contents even more interactive and even allow it to serve as an indication of reading progress. But even if you only add a list of links, it will already be a great feature for the user reading your content.


The post Parsing Markdown into an Automated Table of Contents appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Sticky Table of Contents with Scrolling Active States

Say you have a two-column layout: a main column with content. Say it has a lot of content, with sections that requires scrolling. And let’s toss in a sidebar column that is largely empty, such that you can safely put a position: sticky; table of contents over there for all that content in the main column. A fairly common pattern for documentation.

Bramus Van Damme has a nice tutorial on all this, starting from semantic markup, implementing most of the functionality with HTML and CSS, and then doing the last bit of active nav enhancement with JavaScript.

For example, if you don’t click yourself down to a section (where you might be able to get away with :target styling for active navigation), JavaScript is necessary to tell where you are scrolled to an highlight the active navigation. That active bit is handled nicely with IntersectionObserver, which is, like, the perfect API for this.

Here’s that result:

It reminds me of a very similar demo from Hakim El Hattab he called Progress Nav. The design pattern is exactly the same, but Hakim’s version has this ultra fancy SVG path that draws itself along the way, indenting for sub nav. I’ll embed a video here:

That one doesn’t use IntersectionObserver, so if you want to hack on this, combine ’em!

The post Sticky Table of Contents with Scrolling Active States appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]