Marking external links with CSS

Adding some kind of indicator to links to third party websites is good practice, hinting to the user that they're about to leave the site they're reading. By combining a few CSS tricks and techniques it's possible to add these automatically.

Before we get to the indicators themselves, let's recap some CSS fundamentals that are required to make this happen.

Attribute selectors

While we most commonly select HTML elements with class (.) and ID (#), CSS can select elements via the attribute selector, including classes and IDs.

A good use case for selecting an element by attribute is to change the style of a menu item that points to the page that is currently open. Using the correct aria attribute gives you a reliable way to select without adding a class:

nav a[aria-current='page'] {
  color: var(--color-active);
}

I really like using aria attributes in this way. Accessibility should be integrated from the very beginning, and tightly coupling aria with the styles that apply to that aria attribute puts it front of mind while working.

You don't have to define a value for the attribute. disabled is a boolean attribute; it doesn't need a value, just to be present:

[disabled] {
  color: var(--color-disabled);
}

Wildcard selectors

The above attribute selector would only match to the exact value defined. CSS also provides wildcards in attribute selectors to match all or part of the value.

Starts with

Adding a caret (^) before equals will match any element where the value starts with the specified string.

a[href^='http'] {
  color: var(--color-external);
}
  • Matches <a href="http://geocities.com">.
  • Does not match <a href="/writing">.

Ends with

Adding dollar ($) before equals will match any element where the value ends with the specified string.

a[href$='myspace.com'] {
  color: var(--color-brand-myspace);
}
  • Matches <a href="https://myspace.com">.
  • Does not match <a href="https://myspace.com/tom">.

Contains

Adding an asterisk (*) before equals will match any element where the value contains the specified string.

a[href*='napster.com'] {
  color: var(--color-brand-napster);
}
  • Matches <a href="https://napster.com/metallica">.
  • Does not match <a href="http://limewire.com">.

Pseudo-classes

CSS pseudo-classes are keywords added to a selector to narrow the selection to a particular state of that element. If you have a list of elements you can use :last-child or :last-of-type to target the last item.

We need the negation pseudo-class, which is written as :not. It will match elements that do not match a list of selectors. A use case for this is to add a border to the bottom of every element in a list except the last one:

li:not(:last-child) {
  border-bottom: 1px solid var(--color-border);
}

Pseudo-elements

CSS pseudo-elements are keywords added to a selector to style part of the selected element. A use case for this is to select the ::first-letter of a paragraph and apply some initial styles.

We need the ::after pseudo-element which adds a new element as the last child of the parent element. This can be used to add some text after an element without needing to add that text to the HTML:

.wrong-answer::after {
  content: '✘';
}

.correct-answer::after {
  content: '✔';
}

Marking external links

Let's put all of these CSS techniques together. Before starting a task like this, it's always a good idea to make some notes and establish what the end result should be.

We want to:

  • Select links with absolute paths.
  • Exclude absolute paths that point back to the specified domain.
  • Apply some styles to make those links easy to distinguish.

The selector

a[href^='http']:not([href*='short.is'])::after

It's ugly, but it's just a combination of each selector we've covered. Breaking it down:

  • Select anchor elements, that
  • Have an href attribute that starts with http, but
  • Not elements that have an href attribute that contains short.is, then
  • Apply content after the selected elements.

The styling

a {
  white-space: nowrap;
}

a[href^='http']:not([href*='short.is'])::after {
  content: '↗';
  display: inline-block;
  margin-left: 0.25em;
  text-decoration: none;
}

Let's go over each property and what it's doing:

  • The combination of white-space: nowrap on the anchor, and display: inline-block on the pseudo-element ensures the whole link including the arrow will remain on one line. Without these, you could end up with the arrow on the line below if a link is at the right edge of the viewport.
  • content defines what should be shown on the page after the link text. We've added a small arrow.
  • margin-left just adds a little space to the left of the arrow. Depending on your font and type setting you may need to do a little work here to make it look natural and avoid too much cramp.
  • text-decoration removes the underline from the arrow we've applied to the link. This isn't essential, it's just a stylistic choice.

That's all that's needed to automatically add an indicator to external links.

Going further

Smarter content property

A screen reader announces content in pseudo-elements like any other content in the DOM, so in this case it's not useful or accurate to be told that there is an arrow without context of what that arrow means.

A newer feature in the content property is alternate text, just like the alt attribute on an image. This is added after a forward slash:

@supports (content: 'x' / 'y') {
  content: '↗' / 'Link opens in a new tab';
}

Browser support is good but not complete so this has to be inside a @supports block to avoid breaking the property entirely.

Making the marker dynamic

The text to be rendered after the link can be read from the HTML using a data attribute and the attr() CSS function.

This could be useful to provide an optional override to what is defined in the CSS, like a company name or icon to add some context. This also uses the attribute selector:

a::after {
  content: "↗";
}

a[data-brand]::after {
  content: attr(data-brand);
}

<a href="https://google.com">Google</a>
<a href="https://bing.com" data-brand="Microsoft">Bing</a>

In the future we'll be able to do this without repeating ourselves:

a::after {
  content: attr(data-brand, '↗');
}

The second argument to the attr() function is a fallback value which is rendered if there is no data-brand attribute.

Conclusion

This turned into a bit of a lesson in CSS fundamentals, but it serves as a good example of how a few parts of the language can be combined to make aesthetic changes that require next to no thought once implemented.

CSS is powerful and constantly evolving.

If you have found this article useful please consider buying me a coffee.

More posts

Older

Setting up a new Mac

Newer

Building a better menu with roles and ARIA state