Skip to main content
Home / Articles /

On Adding IDs to Headings

Here’s a two-second review. If an element has an ID, you can link to it with natural browser behavior. It’s great if headings have them, because it’s often useful to link directly to a specific section of content.

<h3 id="step-2">Step 2</a>

Should I be so inclined, I could link right to this heading, be it from an URL, like https://my-website.com/#step-2, or an on-page link, like:

<a href="#step-2">Jump to Step 2</a>

So, it’s ideal if all headers have unique IDs.

I find it entirely too much work to manually add IDs to all my headings though. For years and years, I did it like this using jQuery on this very site (sue me):

// Adjust this for targetting the headers important to have IDs
const $headers = $(".article-content > h3");

$headers.each((i, el) => {
  const $el = $(el);

  // Probably a flexbox layout style page
  if ($el.has("a").length != 0) {
    return;
  }

  let idToLink = "";

  if ($el.attr("id") === undefined) {
    // give it ID
    idToLink = "article-header-id-" + i;
    $el.attr("id", idToLink);
  } else {
    // already has ID
    idToLink = $el.attr("id");
  }

  const $headerLink = $("<a />", {
    html: "#",
    class: "article-headline-link",
    href: "#" + idToLink
  });

  $el.addClass("has-header-link").prepend($headerLink);
});

That script goes one step further than just adding IDs (if it doesn’t already have one) by adding a # link right inside the heading that links to that heading. The point of that is to demonstrate that the headers have IDs, and makes it easy to do stuff like right-click copy-link. Here’s that demo, if you care to see it.

Problem! All the sudden this stopped working.

Not the script itself, that works fine. But the native browser behavior that allows the browser to jump down to the heading when the page loads is what’s busted. I imagine it’s a race condition:

  1. The HTML arrives
  2. The page starts to render
  3. The browser is looking for the ID in the URL to scroll down to
  4. It doesn’t find it…
  5. Oh wait there it is!
  6. Scroll there.

The Oh wait there it is! step is from the script executing and putting that ID on the heading. I really don’t blame browsers for not jumping to dynamically-inserted links. I’m surprised this worked for as long as it did.

It’s much better to have the IDs on the headings by the time the HTML arrives. This site is WordPress, so I knew I could do it with some kind of content filter. Turns out I didn’t even have to bother because, of course, there is a plugin for that: Karolína Vyskočilová‘s Add Anchor Links. Works great for me. It’s technique is that it adds the ID on the anchor link itself, which is also totally fine. I guess that’s another way of avoiding messing with existing IDs.

If I didn’t have WordPress, I would have found some other way to process the HTML server-side to make sure there is some kind of heading link happening somehow. There is always a way. In fact, if it was too weird or cumbersome or whatever to do during the build process or in a server-side filter, I would look at doing it in a service worker. I’ve been having fun playing with Cloudflare’s HTMLRewriter, which is totally capable of this.