challenges

MDX Table of Contents: Part 3 (Solution)

You can find a summary of the code updates in this pull request .

Step 1: Hide the table of contents for narrow viewports

Here we’re going to need a media query that applies different CSS rules when the screen width is >=70rem.

I like to go with a “mobile first” approach , where the default CSS is for narrower screen widths (like mobile devices), and then extra rules are applied within media queries for wider screen widths.

For mobile, the styles are pretty similar to the original BlogPost.module.css file. I reverted to that code temporarily, and added a rule that the table of contents should not be visible.

BlogPost.module.css

aside.toc {
  display: none;
}

Then I added all of the grid styling for screen widths >=70rem:

BlogPost.module.css

@media (min-width: 70rem) {
  .wrapper {
    display: grid;
    grid-template-areas:
      "hero empty"
      "content sidebar";
    gap: 2.5rem;
    grid-template-columns: 1fr 18rem;
  }
 
  .blogHero {
    grid-area: hero;
  }
 
  .content {
    grid-area: content;
  }
 
  aside.toc {
    grid-area: sidebar;
    align-self: start;
  }
}

This almost works — the problem is, all previous CSS rules apply unless they’re overwritten. So the display: none; is still applying to the table of contents. It won’t show up until we override that with display: revert; (the revert means “whatever the value was before it got changed” – in this case, the change was to none).

BlogPost.module.css

  aside.toc {
    display: revert;
    grid-area: sidebar;
    align-self: start;
  }

Step 2: Stickiness

Sticky positioning has a lot of gotchas. I have spent many a tense debugging session trying to get an uncooperative element to stick.

Your first attempt at getting the table of contents to stick might have been similar to mine. In src/components/ToC/ToC.module.css, I added position: sticky. Easy, right?

ToC.module.css

.wrapper {
  position: sticky;
  top: 1rem;
 
  padding: 1rem;
  background-color: var(--color-blue-3);
}

The problem: this doesn’t work. 🤬

Here’s what’s going on. We’re asking the ToC component to “stick” within its parent. In this case, the parent is the aside element in the BlogPost component.

If you add a border to the .wrapper class for ToC (blue dots in these screenshots) and another one to the aside.toc class for the aside in BlogPost (solid red), this is what it looks like:

table of contents with blue dotted outline, and a red solid outline surrounding the blue dotted outline, with no space in between the two outlines

Sticky positioning essentially proposes this: keep the sticky element…

  1. no closer than the specified distance to the viewport (in this case, 1rem from the top)
  2. unless that would mean the sticky element would leave its parent element. In that case, ignore the stickiness.

As you can see in the screenshot above, the sticky element (the table of contents) can’t stay within 1rem of the top of the viewport without leaving its parent (the aside). The table of contents can’t change its position relative to its parent at all without leaving its parent, because they’re the same size. Wherever the aside goes, the table of contents is along for the ride.

The solution here is to expand the size of the aside so that it’s as tall as its container (the .wrapper grid for the BlogPost component). We can do that with a CSS rule of height: 100% for the aside’s class (aside.toc). In BlogPost.module.css:

BlogPost.module.css

@media (min-width: 70rem) {
  /* ... */
  aside.toc {
    display: revert;
    grid-area: sidebar;
    align-self: start;
    height: 100%;
  }
}

Now the table of contents has room to change its position within its parent, aside.

table of contents with blue dotted outline, and a red solid outline that surrounds the blue dotted outline closely on the top and sides, but continues beyond the bottom of the viewport (while the blue dotted outline stops well above the bottom of the viewport) table of contents with blue dotted outline, and a red solid outline that surrounds the blue dotted outline closely on the sides, but the top and bottom of the blue dotted outline are well within the top and bottom of the red solid outline

Step 3: Account for the header height

If you’ve been coding along, you might have noticed that your app doesn’t quite look like the images above. The table of contents is sticking 1rem from the top of the viewport, which means it gets buried under the header like this:

table of contents with the top hidden behind the page header

We need to account for the header height in the top distance from the viewport. Fortunately, there’s a CSS custom property for the height of the header in src/app/globals.css. We can add it to the 1rem by using calc .

ToC.module.css

.wrapper {
  position: sticky;
  top: calc(1rem + var(--height-header));
 
  /* ... */
}

Voila! We’ve achieved the goals. See you in the next workshop!