Skip to main content

Are your Anchor Links Accessible?

12 mins 📖 📖 📖 📖 📖 📖 

Room for growth

accessibility create ssg

One day I decided I wanted to add anchor links to each of the sections in my blog posts. Over the past few months, I'd seen these links on a lot of pages—from official documentation pages to smaller blog posts. I find them really useful for sharing sections of pages with other people!

To make the different sections of my blog posts shareable, I decided to add an anchor link to all of the section headers (<h2>s) in each of my posts.

While researching the best way to do this, something I noticed again and again was that other sites had almost always implemented anchor links in an inaccessible way.

Within a few hours, I had made a plugin to automate the addition of anchor links to my blog post sections, AND I had ensured the links were accessible!

Anchor links provide a way to link to separate sections of a page. On this page, hover over the level two headings on desktop to see the links (the links are always visible on smaller screens).

In essence, an anchor link is a link containg a URL fragment. This fragment usually appears at the end of a URL. It begins with a hash character (#), and is followed by a string. This string identifies a section in a web page.

For example, a page may have a section containing a section header element such as an <h2>. This element can be given an id attribute. An anchor link is created when the value of the id matches the href value of an anchor (<a>) element on the same page. Consider the following code:

<h2 id="introduction">Introduction</h2>
<a href="#introduction">#</a>

Imagine the web page containing this code had the following URL: https://example.com/blog/nice-post/. Clicking on the hash character within the <a> element would do two things; the browser window would scroll to the beginning of the introduction section, and the URL would become https://example.com/blog/nice-post/#introduction. This URL can be shared and when opened in a browser, the page will automatically scroll to the introduction section.

Of course, I wanted to make sure the anchor links I had written were accessible. I turned on VoiceOver (a screen reader for MacOS), and using caps lock with arrow keys to interact with the page's content, I moved to my section heading. I found a few issues. Below is the first version of my anchor link:

<h2 id="introduction"><a href="#introduction">#</a>Introduction</h2>

When the <a> element is inside the <h2>, VoiceOver reads out "heading level 2 2 items, visited, link number introduction". A screen reader user who wanted to skip across multiple headings at once would have a nicer experience if they only heard the actual heading text (i.e. "introduction").

I realised the <a> element should exist as a sibling of the heading element, rather than a child of it. This is so that the <h2> text content remains clear. Below is the second version of my anchor link:

<a href="#introduction">#</a>
<h2 id="introduction">Introduction</h2>

When the <a> element is outside the <h2>, VoiceOver reads out "link number" for the <a> and "heading level two introduction" for the <h2>. This is a problem, as the elements are not associated with each other in any way.

Associating anchor links with what they link to is important. Some anchor links I found on a popular site have SVGs with no labels or titles as content. When there is no information about a link available, the screen reader falls back to the href value (e.g. https://example.com/blog/nice-post/). This makes the purpose of the link hard to decipher.

I realised I needed to better associate my section headings with their anchor links. Below is the third version of my anchor link:

<a aria-label="link to this heading" aria-describedby="introduction" href="#introduction">#</a>
<h2 id="introduction">Introduction</h2>

The attributes I added are aria-label and aria-describedby.

The aria label is read out by a screen reader in place of whatever child the <a> element has. This is necessary for anchor links, as these links often have a single character as text content. One alternative is to describe the link using text content, but visually hide it (hint: I removed this attribute in a later version of my anchor link - refer to the fifth version).

The aria describedby matches the id of the <h2>. When the link is focused, "link, link to this heading" is read out, followed by "introduction". This is much better, but there could be one more small improvement—having the heading before the link (hint: I removed this attribute in a later version of my anchor link - refer to the fifth version).

I realised that my anchor links would make more sense if the heading came before the link. Below is the fourth version of my anchor link:

<h2 id="introduction">Introduction</h2>
<a aria-label="link to this heading" aria-describedby="introduction" href="#introduction">#</a>

VoiceOver reads the <h2> out as "heading level 2 introduction" and the <a> as, "link, link to this heading, introduction". When the heading is first, screen reader users may be able to associate a section heading more easily with its corresponding anchor link.

While I made great progress by the fourth version of my anchor link, there are final some outstanding issues that I wanted to address. Thanks to my friend Kitty for helping me realise these outstanding issues. Be sure to keep up to date with Kitty's current A11y Advent Calendar and check out their other great posts.

First, aria-label is not actually brilliant for accessibility, as some translation services can have trouble accessing its value. So, it's best to add text content to the <a> element, and make sure to visually hide it. This way, screen readers can still access it but it'll be visually hidden for other users. Second, the # can be left in and hidden from screen readers using aria-hidden, while still being visible on the page.

Lastly, I removed aria-describedby because the anchor link's text content now references the section heading.

Here is the fifth version of my anchor link:

<h2 id="introduction">Introduction</h2>
<a href="#introduction">
<span aria-hidden="true">#</span>
<span class="hidden">Section titled introduction</span>
</a>

This fifth and final version of my anchor link is the one I am using on this page, along with a wrapper <div> for styling purposes. It is much better than the first version! However, if you can think of improvements, please let me know.

Edit (17th December): Beware, there be dragons! I realise that including a link (that has its own text content) as a child of a heading may seem tempting (as it can make styling an anchor link easier). However, it's worth repeating that this can harm accessibility.

Version one of my example anchor link (see accessibility check) introduces the issues caused by placing a link inside a heading, but I want to provide some more context to why it is not a good idea! I'll do this by describing a handy feature of VoiceOver.

VoiceOver (and likely also other screen readers) gives a nice overview of a page's section headings. This feature, called the web rotor, is accessed by default by pressing caps lock + u while VoiceOver is on. Below are two screenshots of the web rotor comparing how headings are formed, depending on whether a link is inside or not:

web rotor
VoiceOver web rotor displaying headings containing links
web rotor
VoiceOver web rotor displaying headings without links

If a link that has its own content is placed within a heading, the computed heading text may be more difficult for a screen reader user to understand. This can affect how clear a page's structure is.

Edit (18th December): Despite warning against the following option above, I want to write about making a whole heading into an anchor link. Or, more specifically, wrapping the heading text content in a link. Several people across different sites have asked me why I didn't mention this option before:

I think both options—using an icon as a link, and making the header a link (when the content of the link is the heading text)—can be valid. However, I have a few concerns with wrapping a heading's text content in a link:

  • It can be useful separate the anchor link from the heading so that the link can have its own text content
  • It may be harder to select heading text wrapped in a link
  • Both screen reader and non-screen reader users may find this option less familiar and not understand its purpose

Here are two examples of sites that implement anchor links where a link wraps the text content of a heading: MDN Web Docs and The Web Almanac.

Here is a code example of an accessible anchor link where a link wraps the text content of a heading:

<h2 id="introduction">
<a href="#introduction">
Introduction
</a>
</h2>

Would you rather implement anchor links in this way? It is a simpler solution. However, it is also less flexible.

Of course, the way a feature is implemented depends on your own personal use case. But, no matter which way you implement something, please remember to think of the user experience!

I use Eleventy together with markdown-it to convert my markdown files into HTML files.

I tried finding a plugin that would help me to automate the addition of anchor links for all my <h2> elements. I did find one but soon realised it didn't make accessible links possible. So, I decided to make my own plugin.

There is handy markdown-it developer documentation for people wanting to create plugins. Using these docs, a markdown-it demo page, and some inspiration from a plugin called markdown-it-anchor, I wrote a plugin.

Here is a permalink to my plugin file. It recreates parsing functions for opening and closing <h2> elements from the markdown-it library. In my eleventy.config file, I use (connect) the plugin with markdown-it. This allows me to automatically add custom anchor links for all my level two headings.

A few people have been inspired by this post and one of them is Nicolas Hoizey. In February 2021, Nicolas created an issue in markdown-it-anchor's GitHub repository, hoping the maintainers would consider making changes.

What followed was a really good discussion among several people, kicked off by Valérian Galliat, regarding the solution I decided on in this blog post. I definitely recommend reading through the comments because there are some great nuggets of information about accessibility in there!

Here are some criticisms of the approach to accessible anchor links that I took in this blog post:

  • The text hidden with CSS creates a large gap between heading and paragraphs in Firefox reader mode. However, this could be seen as a reader mode limitation.
  • There is an SEO concern that text usually hidden with CSS shows up in descriptions of posts in Google. However, anchor links will most likely not being reported as illegitimate content.
  • Text usually hidden with CSS also shows up in RSS readers.

Some suggested that aria-label could be used to offer accessible text without the need for aria-labelledby. Kitty reminded us that aria-label is notoriously bad with content translating services such as Google Translate.

I learned a lot from the discussion. I still think there is not really a silver bullet to creating accessible anchor links that would suit everyone. There are quite a few ways to approach it, each with their own advantages and disadvantages, based on things like browser support, screen size, screen readers, and more.

However, Barry Pollard made a good point. His idea is that there are several advantages to making headings into links themselves. I would be happy to use this approach on my own site. Try to hover or focus a heading on a Web Almanac post to see the approach first hand.

What do you think? I'd love to know if you have any ideas. If so, please comment on the GitHub Issue!

Buy me a coffee ❤️

118 Responses

 
Ramón HuidobroPhil WolstenholmeDana ByerlyEva - includeJS.dev 🌈Jan LehnardtEric EggertKilian ValkhofStephanie Eckles - Merry CSSmas 🤶 + 15 more

4 Reposts

calvinRamón HuidobroEric EggertIoanna Talasli

9 Replies

  1. Dana Byerly Dana Byerly
    Thanks so much for this post, it's been on my list to figure out how to do this, and now it's on my site thanks to your post!
  2. Eric Eggert Eric Eggert
    Thanks Amber. Learning by doing is really powerful and writing down learnings helps others to do the same. (And thanks for another blog for my RSS reader! 😃)
  3. Ohh I need to update to this solution - great work!
  4. Darek Kay Darek Kay
    Thanks @ambrwlsn90 , great post! One more thing you could address are touch devices. You solved this by always displaying anchor links on small breakpoints, but this excludes touch devices with big screens.
  5. Michael Maclean Michael Maclean
    Oh cool - I just added those links to my site recently using the same markup as GitHub seemed to, but all the links are aria-hidden=true. I’ll see if I can improve them now
  6. Marc Littlemore 🤖 Marc Littlemore 🤖
    Hey Amber! Yes, I did - thank you. I was looking to add GitHub style anchors and a few articles pointed at your article as being the definitive accessible solution. I was unsure how to style them correctly too and your 11ty markdown renderer code was really helpful. Thanks!
  7. Seirdy Seirdy
    @walterIMHO, your implementation seems just right.Thanks! I based my approach off of Amber Wilson’s section permalinksOne key difference: I wanted CSS to be an optional cosmetic enhancement, and not something that changes the content that people see (except for print media). I want my markup to only define structure/semantics, when possible (i.e. ideally no cosmetic div wrappers). That meant displaying the section permalink as a readable link. I used aria-labelledby to give each section permalink a unique accessible name.I’ve heard positive feedback from both screen-reader and textual-browser users.As for how this relates to reading mode implementations:The point of reading-mode tools is to reduce clutter and focus on reading an article, without the author’s supplied user-interface. Section permalinks feel like a part of a “user interface” and should be removed; the interface should only be provided by the reading-mode. On the other hand, most reading modes don’t provide a document outline or a way to get a link to the current section, and users might want that functionality without having to leave reading-mode. On a third hand: if I include section permalinks in reading mode, then it’d end up looking almost identical to the un-distilled page. That’d make reading mode almost useless.#POSSE note from https://seirdy.one/notes/2022/07/13/section-permalinks/ posse Are your Anchor Links Accessible?
  8. ambrwlsn ambrwlsn
    @zachleat glad you found it helpful! Writing and discussing it was lots of fun and it seems to have resonated with people!
  9. Nicolas Hoizey Nicolas Hoizey

35 Mentions

  1. Baldur Bjarnason Baldur Bjarnason
    “Are your Anchor Links Accessible? - Amber Wilson” amberwilson.co.uk/blog/are-your-…
  2. Lobsters Lobsters
  3. Adactio Links Adactio Links
    Are your Anchor Links Accessible? | Amber Wilson amberwilson.co.uk/blog/are-your-…
  4. Stuart Robson Stuart Robson
    Love this! Are your Anchor Links Accessible? from @ambrwlsn90 amberwilson.co.uk/blog/are-your-…
  5. Mike Harley, Goblin Herder Mike Harley, Goblin Herder
    Are your Anchor Links Accessible? | Amber Wilson amberwilson.co.uk/blog/are-your-…
  6.  + 30 more

1 Bookmark

  1. Jeremy Keith Jeremy Keith
    Are your Anchor Links Accessible? | Amber Wilson December 15th, 2020 I really like the way that Amber doesn’t go straight to the end solution but instead talks through her thought process when adding a feature to her site.