Tag: Table

Faking Min Width on a Table Column

The good ol’ <table> tag is the most semantic HTML for showing tabular data. But I find it very hard to control how the table is presented, particularly column widths in a dynamic environment where you might not know how much content is going into each table cell. In some cases, one column is super wide while others are scrunched up. Other times, we get equal widths, but at the expense of a column that contains more content and needs more space.

But I found a CSS tricks-y workaround that helps make things a little easier. That’s what I want to show you in this post.

The problem

First we need to understand how layout is handled by the browser. We have the table-layout property in CSS to define how a table should distribute the width for each table column. It takes one of two values:

  • auto (default)
  • fixed

Let us start with a table without defining any widths on its columns. In other words, we will let the browser decide how much width to give each column by applying table-layout: auto on it in CSS. As you will notice, the browser does its best with the algorithm it has to divide the full available width between each column.

If we swap out an auto table layout with table-layout: fixed, then the browser will merely divide the full available space by the total number of columns, then apply that value as the width for each column:

But what if we want to control the widths of our columns? We have the <colgroup> element to help! It consists of individual <col> elements we can use to specify the exact width we need for each column. Let’s see how that works in with table-layout: auto:

I have inlined the styles for the sake of illustration.

The browser is not respecting the inline widths since they exceed the amount of available table space when added up. As a result, the table steals space from the columns so that all of the columns are visible. This is perfectly fine default behavior.

How does <colgroup> work with table-layout: fixed. Let’s find out:

This doesn’t look good at all. We need the column with a bunch of content in it to flex a little while maintaining a fixed width for the rest of the columns. A fixed table-layout value respects the width — but so much so that it eats up the space of the column that needs the most space… which is a no-go for us.

This could easily be solved if only we could set a min-width on the column instead of a width. That way, the column would say, “I can give all of you some of my width until we reach this minimum value.“ Then the table would simply overflow its container and give the user a horizontal scroll to display the rest of the table. But unfortunately, min-width on table columns are not respected by the <col> element.

The solution

The solution is to fake a min-width and we need to be a bit creative to do it.

We can add an empty <col> as the second column for our <colgroup> in the HTML and apply a colspan attribute on the first column so that the first column takes up the space for both columns:

 <table>   <colgroup>     <col class="col-200" />     <col />     <col class="col-input" />     <col class="col-date" />     <col class="col-edit" />   </colgroup>      <thead>     <tr>       <th colspan="2">Project name</th>       <th>Amount</th>       <th>Date</th>       <th>Edit</th>     </tr>   </thead>      <!-- etc. --> </table>

Note that I have added classes in place of the inline styles from the previous example. The same idea still applies: we’re applying widths to each column.

The trick is that relationship between the first <col> and the empty second <col>. If we apply a width to the first <col> (it’s 200px in the snippet above), then the second column will be eaten up when the fixed table layout divides up the available space to distribute to the columns. But the width of the first column (200px) is respected and remains in place.

Voilà! We have a faux min-width set on a table cell. The first cell flexes as the available space changes and the table overflows for horizontal scrolling just as we hoped it would.

(I added a little sticky positioning to the first column there.)


Let’s not totally forget about accessibility here. I ran the table through NVDA on Windows and VoiceOver on macOS and found that all five columns are announced, even if we’re only using four of them. And when the first column is in focus, it announces, “Column one through two”. Not perfectly elegant but also not going to cause someone to get lost. I imagine we could throw an aria-hidden attribute on the unused column, but also know ARIA isn’t a substitute for poor HTML.

I’ll admit, this feels a little, um, hacky. But it does work! Let me know if you have a different approach in the comments… or know of any confusions this “hack” might bring to our users.

Faking Min Width on a Table Column originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.


, , ,

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!


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.


, , ,

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


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


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.


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.


, , , , ,

TablesNG — Improvements to table rendering in Chromium

When I blogged “Making Tables With Sticky Header and Footers Got a Bit Easier” recently, I mentioned that the “stickiness” improvement was just one of the features that got better for <table>s in Chrome as part of the TablesNG upgrade. I ain’t the only one who’s stoked about it.

But Bramus took it the rest of the nine yards and looked at all of the table enhancements. Every one of these is great. The kind of thing that makes CSS ever-so-slightly less frustrating.

Just the writing-mode stuff is fantastic.

Direct Link to ArticlePermalink

The post TablesNG — Improvements to table rendering in Chromium appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, , , ,

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.


, ,

A table with both a sticky header and a sticky first column

We’ve covered that individual <table> cells, <th> and <td> can be position: sticky. It’s pretty easy to make the header of a table stick to the top of the screen while scrolling through a bunch or rows (like this demo).

But stickiness isn’t just for the top of the screen, you can stick things in any scroll direction (horizontal is just as fun). In fact, we can have multiple sticky elements stuck in different directions inside the same element, and even single elements that are stuck in multiple directions.

Here’s a video example of a table that sticks both the header and first column:

Why would you do that? Specifically for tabular data where cross-referencing is the point. In this table (which represents, of course, the scoring baseball game where somehow 20 teams are all playing each other at once because that’s how baseball works), it “makes sense” that you wouldn’t want the team name or the inning number to scroll away, as you’d lose context of what you’re looking at.

Not all tables need to be bi-directionally cross-referenceable. A lot of tables can smash rows into blocks on small screens for a better small-screen experience.

The “trick” at play here is partially the position: sticky; usage, but moreso to me, how you have to handle overlapping elements. A table cell that is sticky needs to have a background, because otherwise we’ll see overlapping content. It also needs proper z-index handling so that when it sticks in place, it’ll be on top of what it is supposed to be on top of. This feels like the trickiest part:

  • Make sure the tbody>th cells are above regular table cells, so they stay on top during a horizontal scroll.
  • Make sure the thead>th cells are above those, for vertical scrolling.
  • Make sure the thead>th:first-child cell is the very highest, as it needs to be above the body cells and it’s sibling headers again for horizontal scrolling.

A bit of a dance, but it’s doable.

High five to Cameron Clark who emailed me demoed this and showed me how cool it is. And indeed, Cameron, it is cool. When I shared that around, Estelle Weyl showed me a demo she made several years ago. That feels about right, Estelle is always a couple of years ahead of me.

The post A table with both a sticky header and a sticky first column appeared first on CSS-Tricks.

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


, , , , ,

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 } 

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.


, , , , ,

Rotated Table Column Headers… Now With Fewer Magic Numbers!

Rotated <table> column headers is something that’s been covered before right here on CSS-Tricks, so shout-out to that for getting me started and helping me achieve this effect. As the article points out, if you aren’t using trigonometry to calculate your table styles, you’ll have to rely on magic numbers and your table will be brittle and any dreams of responsiveness crushed. 

Fortunately, in this case, we can take the trigonometry out and replace it with some careful geometry and our magic numbers all turn into 0 (a truly magical number).

For those in a hurry, here is the CSS (it’s very similar to the styles in the other article). Below is a thorough walk-through.

<th class="rotate"><div><span>Column Header 1</span></div></th>
table {  border-collapse: collapse;  --table-border-width: 1px; } th.rotate {   white-space: nowrap;   position: relative; 
} th.rotate > div {   /* place div at bottom left of the th parent */   position: absolute;   bottom: 0;   left: 0;   /* Make sure short labels still meet the corner of the parent otherwise you'll get a gap */   text-align: left;   /* Move the top left corner of the span's bottom-border to line up with the top left corner of the td's border-right border so that the border corners are matched    * Rotate 315 (-45) degrees about matched border corners */   transform:      translate(calc(100% - var(--table-border-width) / 2), var(--table-border-width))     rotate(315deg);   transform-origin: 0% calc(100% - var(--table-border-width));   width: 100%; 
} th.rotate > div > span {   /* make sure the bottom of the span is matched up with the bottom of the parent div */   position: absolute;   bottom: 0;   left: 0;   border-bottom: var(--table-border-width) solid gray; } td {   border-right: var(--table-border-width) solid gray;   /* make sure this is at least as wide as sqrt(2) * height of the tallest letter in your font or the headers will overlap each other*/   min-width: 30px;   padding-top: 2px;   padding-left: 5px;   text-align: right; }

Let’s unpack this table and see what’s going on. The magic starts with that funny chain of HTML tags. We’re putting a <span> inside of a <div> inside of our <th>. Is this all really necessary? Between how borders behave, the positioning flexibility we need, and what determines the width of a table column… yes, they each have a purpose and are necessary.

Let’s see what happens if we rotate the <th> directly:

<th class="rotate">Column header 1</th>
table {   border-collapse: collapse; } th.rotate {   border-bottom: 1px solid gray;   transform: rotate(315deg);   white-space: nowrap; } td {   border-right: 1px solid gray;   min-width: 30px;   padding-top: 2px;   padding-left: 5px;   text-align: right; }

Ignoring the fact that we haven’t corrected position, there are two big issues here: 

  1. The column width is still calculated from the header length which is what we were trying to avoid.
  2. Our border didn’t come with us in the rotation, because it is actually part of the table.

These problems aren’t so difficult to fix. We know that if the <th> has a child element with a border, the browser won’t treat that border as part of the table. Further, we know that absolutely-positioned elements are taken out of the document flow and won’t affect the parent’s width. Enter <div> tag, stage left…and right, I guess.

<th class="rotate"><div>Column header 1</div></th>
table {   border-collapse: collapse; } th.rotate {   white-space: nowrap;   position: relative; } th.rotate > div {   position: absolute;   transform: rotate(315deg);   border-bottom: 1px solid gray; } td {   border-right: 1px solid gray;   min-width: 30px;   text-align: right;   padding-top: 2px;   padding-left: 5px; }
Now our headers don’t influence the column width and the borders are rotated. We just need to line things up.

It’s easier to tell in the image with the rotated <th> elements, but that rotation is happening around the center of the element (that’s the default behavior of transform-origin). It is only another transform in x and y to get it to the right spot, but this is where we’d need trigonometry to figure out just how much x and y to line it up with the column borders. If we instead carefully choose the point to rotate the header about, and use transform-origin to select it, then we can end up with distances that are more straightforward than magic numbers.

The animation below helps illustrate what we’re going to do to avoid complicated math. The black dot in the top left of the blue border needs to match the red dot on the right border of the table column and rotate about it. Then there won’t be any gaps between the two borders.

It’s not helpful to start going somewhere if you don’t know where you are. The absolute positioning is going to help us out with this. By specifying bottom: 0; left: 0; on the <div>, it ends up at the bottom left of the parent <th>. This means the <div> border’s bottom-left corner is sitting on top of the left column border and halfway through it. From here, it’s apparent we need to move down one border width and over one cell width, but how are we going to get that responsively? It’s at this very moment you may recall that we haven’t added the <span> yet — we’re going to need it!

We’ll use the <div> to “figure out” how big the table cells are and the <span> to actually hold the text and position it absolutely as well to overflow the parent.

<th class="rotate"><div><span>Column header 1</span></div></th>
th.rotate{   white-space: nowrap;   position: relative; } th.rotate > div {   position: absolute;   bottom: 0;   left: 0;   width: 100%;  /* <- now the div parent is as wide as the columns */ } th.rotate > div > span {   position: absolute;   bottom: 0;   left: 0;   border-bottom: 1px solid gray; }

Great! When we set the width of the <div> to 100%, it holds the information for how big the column is regardless of what the content is in the table cells. With this in place, we can easily translate things over by the width of the <div> — but don’t forget that we need to shave off a half border width. Our translation becomes:

transform: translate( calc( 100% - var(--table-border-width)/2), var(--table-border-width));

The <div> is now in the right spot to rotate, but we have to make sure to pick the correct transform-origin. We want it to be on the top-left corner of the border, which will be on the left and up one border’s width from the bottom of our <div> element:

transform-origin: 0%, calc(100% - var(--table-border-width));

This brings us to our final style for the table header.

table {   border-collapse: collapse;   --table-border-width: 1px; } th.rotate{   white-space: nowrap;   position: relative; } th.rotate > div {   position: absolute;   bottom: 0;   left: 0;   width: 100%;   transform:     translate( calc( 100% - var(--table-border-width)/2), var(--table-border-width));     rotate(315deg);   transform-origin: 0%, calc(100% - var(--table-border-width)); } th.rotate > div > span {   position: absolute;   bottom: 0;   left: 0;   border-bottom: var(--table-border-width) solid gray; }

Note that transformations happen after everything is placed. That means the rotated headers will overflow onto everything as best they can. You will need to wrap the whole table in something to compensate for the unexpected height. I put the title and table together in a flexbox <div> and set the flex-basis of the title to a value large enough to compensate for the tall headers.

#div-with-table {   display: flex;   flex-direction: column;   justify-content: space-around; } #title {   flex-basis: 140px; }

The post Rotated Table Column Headers… Now With Fewer Magic Numbers! appeared first on CSS-Tricks.


, , , , , ,

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.


, , , , ,

Table with Expando Rows

“Expando Rows” is a concept where multiple related rows in a <table> are collapsed until you open them. You’d call that “progressive disclosure” in interaction design parlance.

After all these years on CSS-Tricks, I have a little better eye for what the accessibility concerns of a design/interactivity feature are. I’m not entirely sure how I would have approached this problem myself, but there is a good chance that whatever I would have tried wouldn’t have hit the bullseye with accessibility.

That’s why I’m extra happy when someone like Adrian Roselli tackles problems like this, because the accessibility is handled right up front (see the videos in all the major screen readers).

I feel the same way when we get demos from Scott O’Hara, Lindsey Kopacz, and Hedyon Pickering.

See the Pen
Table with Expando Rows
by Adrian Roselli (@aardrian)
on CodePen.

Direct Link to ArticlePermalink

The post Table with Expando Rows appeared first on CSS-Tricks.


, ,