Skip to content

Daniel Lamb Byte–size Bits & Bobs

Automate Article Deep Linking

Tiny script to dynamically inject heading links

6 minute read.

Update: I have addressed a couple issues with this implementation in a follow–up post.

Especially with longer articles it is very useful to be able to share a link to a particular section.

As you probably already know, browsers automatically support scrolling to a section of the page based on an identifier in the HTML. Simply include an ID in the URL hash (#awesome) and the browser will open the page with that element <div id="awesome"> at the top.

I use a slightly modified version of this code on this blog, hover over the heading above to see it in action.

This blog is generated using kramdown1 which automatically generates header IDs from the heading text. For example, the markdown:

## Super Awesome

Gets is converted into the following HTML:

<h2 id="super-awesome">Super Awesome</h2>

Sweet! So all I need to do is inject anchor tags with the IDs and I’m done.

Dynamically Injecting Anchor Tags

This tiny snippet of JavaScript (0.1 kb minified) is all you need to automatically link page headings.

var headings = document.querySelectorAll('h2,h3,h4,h5,h6');
[].forEach.call(headings, function(heading) {
  if (heading.id) {
    var anchor = '<a href="#' + heading.id + '" class="anchor">¶</a>';
    heading.innerHTML = anchor + heading.innerHTML;
  }
});

How the Code Works

While there is not a great deal of code, it contains couple more advanced tricks, so I have included a flow chart to clarify what it does and why.

1. Select headings on the pageheading2. Loop using forEach3. Has ID?5. Skip4. Prepend contents with anchorYesNext
  1. Select all the sub headings on the page using querySelectorAll2.
  2. Use the native Array method forEach3 to loop through the selected headings. I use this method for two reasons:
    1. First list returned in step 1 is a NodeList4 not an Array so it enumerates length and item properties not just the elements.
    2. Secondly the node list is a live connection meaning DOM changes are reflected in the list. This makes looping slower than it needs to be since you are unnecessarily checking the DOM for changes.
  3. Check if the heading tag has an ID attribute.
  4. Use the heading ID in the href of the injected anchor.
  5. Skip headings that do not have an id.

The code above works to dynamically inject the links, but it’d be nice to style the anchors to only show when you hover over the headings. You’ll probably have to tweak things based on the CSS context you’re working with, but something like the following SCSS5 will help get you started.

// unobtrusively style heading anchors
.anchor {
  top: 0;
  opacity: 0;
  color: #ccc;
  left: -1rem;
  position: absolute;  
  padding-right: 0.5rem;
  transition: opacity 0.2s ease-in-out 0.1s;
}
h2, h3, h4, h5, h6 {
  position: relative;
  &:hover .anchor {
    opacity: 1;
    color: #ccc;
    text-decoration: none;
  }
}

Styling Breakdown

I tried a few variation of the styles above and I’m certain there are other better ways to do this. However, I’ll cover some of the notable choices I made next.

  • position:absolute; so that the headings don’t shift as the anchor tags are injected and the styles are applied.
  • opacity: is used for two reasons:
    1. First since the element is always there (only hidden) it’s possible to hover over the anchor on it’s own, not just the heading.
    2. Secondly it allows for the option to animate the transition. That wouldn’t be possible if I used display:none.
  • padding-right: is important because of the negative left value used to possition the anchor. Without padding the anchor may inadvertently hide when the mouse moves in-between the heading and the anchor.
  • rem6 units are used to support responsive text sizes.

Improvements

This script currently assumes it will be run on a fairly modern browser. For example, I don’t ensure that array forEach and querySelectorAll methods exist.

  1. kramdown is a fast Markdown converter.

  2. querySelectorAll returns all elements that match the provided selector(s).

  3. forEach executes a callback once for each element in an array.

  4. NodeLists are array like but have important differences.

  5. SCSS Stands for “Sassy CSS” and is a superset of CSS3’s syntax.

  6. rem refers to the font-size of the root element, not rapid eye movement.


Give Feedback via my AMA (ask me anything) project on GitHub. Published on and last modified on by Daniel Lamb.
You may also enjoy these posts