Skip to main content Accessibility Feedback

How to add icons to external links with CSS

As part of my ongoing updates to my personal site, I decided to add icons after all external links.

Today, I wanted to show you how I did it. Let’s dig in!

Why?

I do a lot of linking on my site, and it’s not always obvious when a link will keep you on-site vs. take you somewhere else.

Using [target="_blank"] forces links to open in a new tab or window, but this is considered an anti-pattern for accessibility reasons. It can be confusing, and it robs users or choice and agency.

An icon lets them make an informed decision.

Rather than manually adding a link to every external link, I figured out how to target all links that point to an external page and add an icon with CSS instead.

Modern CSS makes this possible!

I want to target…

  1. Links that start with http (not relative links that point to the current site), and
  2. Do not have the .btn class (the class I use to style links as buttons), and
  3. Do not have an SVG icon inside them already, and
  4. Do not have my site URL or localhost in the URL.

I can use the starts-with attribute selector (^=) to check for links that start with specific patterns (like http).

The :not() and :has() pseudo-classes let me exclude certain selectors.

[href^="http"]:not(.btn, :has(svg)):not([href^="https://gomakethings.com"], [href^="http://localhost"]) {
	/* ... */
}

Adding Content

The ::after pseudo-element lets me add content after the targeted link.

[href^="http"]:not(.btn, :has(svg)):not([href^="https://gomakethings.com"], [href^="http://localhost"])::after {
	/* ... */
}

While you can add an SVG icon using background-image, you lose the ability to have it inherit fill color with the currentColor property and sizing relative to the target link.

To get around this, we need to instead use the mask and background-color properties.

[href^="http"]:not(.btn, :has(svg)):not([href^="https://gomakethings.com"], [href^="http://localhost"])::after {
	background-color: currentColor;
	mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg>');
	mask-size: cover;
}

We also need to include an empty content string, a display property, and a height and width to ensure the icon actually occupies space in the DOM.

[href^="http"]:not(.btn, :has(svg)):not([href^="https://gomakethings.com"], [href^="http://localhost"])::after {
	background-color: currentColor;
	content: "";
	display: inline-block;
	mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg>');
	mask-size: cover;
	height: 0.8em;
	width: 0.8em;
}

Odds & Ends

A few final-but-important details.

The icon may visually indicate “external link” to sighted users, but will not announce anything to visually impaired users. The content property has an alt text option: <content> / <alt text>.

Let’s add that with (external link) as the text.

[href^="http"]:not(.btn, :has(svg)):not([href^="https://gomakethings.com"], [href^="http://localhost"])::after {
	background-color: currentColor;
	content: "" / "(external link)";
	display: inline-block;
	mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg>');
	mask-size: cover;
	height: 0.8em;
	width: 0.8em;
}

The icon I chose also sits slightly off center. I use some margins to nudge-and-tweak it to where I want it to actually be.

[href^="http"]:not(.btn, :has(svg)):not([href^="https://gomakethings.com"], [href^="http://localhost"])::after {
	background-color: currentColor;
	content: "" / "(external link)";
	display: inline-block;
	margin-block-end: -0.1ch;
	margin-inline-start: 0.5ch;
	mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg>');
	mask-size: cover;
	height: 0.8em;
	width: 0.8em;
}

Here’s a demo on CodePen that you can play with!

Feel free to steal this, swap in your own icon, adjust the target URLs, and so on.