Accessible icon buttons with masks and SVG.

In this article we'll see how to make fully accessible icon buttons using SVG and CSS, without bloating the html with the inline <svg> code but keeping the customization!

But first, let's define what we want to achieve with our code, what we want from the final ouput of our icon button element:

...and we will achieve all of this points with just HTML and CSS! It's magic, isn't it?

The markup

A <button> element is everything we need, you never would have said that, right?

<button aria-label="Download File"></button>

If you don't like to use an empty element, you can use your .sr-only utility class to hide the inside content from the UI.

<button>
<span class="sr-only">Download File</span>
</button>

You can choose to use the aria-label attribute to add a mean to the button or the hidden element readable only by assistive technologies, it's up to you. Using the button element we already solved the first three points of our checklist since it is interactive, keyboard navigable and accessible by default. No tabindex needed or javascript code to make it keyboard accessibile.

Customization

So, how we can change colors, sizes, add gradients using a single SVG image from our CSS? The answer is...using the very wide supported mask-* properties!

The only constraint is that the source icon should be black on a trasparent or white background. That's because the mask property uses the pure black color to shows the visible part of the background, while the white or transparent color is used to obsfuscale the background layer.

Let's start adding some basic style to our button to set up positioning and box model:

.IconButton {
width: 56px;
height: 56px;
appearance: none;
border-radius: 50%;
border: 0;
background-color: #000;
cursor: pointer;
padding: 0;
position: relative;
}

Now, we are going to use a ::pseudo-element to add the icon. The absolute positioned pseudo element is useful to mathematically centering the icon, but at the same time, it allows us to optically center the icon when we need to do it.

.IconButton::before {
/* Positioning related code */
content: "";
display: block;
width: 50%;
height: 50%;
background: #000;
pointer-events: none;

/* this allows optical centering if required */
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}

We can now add our magic code. First of all we need an icon, and we need it as SVG since we want it to scale without loosing quality. We will use this icon for this example.

Using the mask- properties we can set our icon as element mask, like we do with design tools like Sketch, Figma and others. Let's add these properties:

.IconButton::before {
/* Previous code... */

/* Masking code */
mask-image: url("https://cdn.glitch.com/a10b6199-a788-4cef-8bd8-21f95dbcba93%2Fdownload-cloud.svg?v=1571346192759");
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
}

As you can see we defined four things here:

You might have noticed that these properties are the same (and act like) of the background- properties. The main difference here is that the image is used as mask and makes our element "transparent" where the image is black, while the white (or transparent) part of the image is used to hide what's on the background. Our icon here is white because we set background: #fff; on the pseudo element and the svg icon is filled with #000.

Now, we should see something like this:

accessible-icon-button

This way you can change the background, add gradients ad make fading-out icons by filling them with a gradient from black and to a transparent color. Inside the mask-image you can also use an icon from your sprite SVG file! Pretty awesome and scalable!

mask-image: url("path/to/sprite.svg#my-icon-id");

Let's see some example using different icons and changing the ::before background:

examples

The first button uses a full black svg icon, the second one adds a linear gradient to the ::pseudo-element and the third one uses an icon filled with a gradient from black to a transparent color. As always, you can even animate whatever you want and you can combine custom properties to pass the icon path from the html and handle them easily with javascript using .setProperty(), for example:

myelement.style.setProperty("--icon", "...");

We can now flag the last step! 🎉

Live demo

Progressive enhancement

The mask- properties are well supported by all the modern browser, but if for some weird reason you need to support obsolete browsers that doesn't support these properties, you can progressive enhance the code in order to provide a minimal UI experience.

Be aware that when talking about "minimal UI experience" we mean that things may not look the same, but they just work as expected without breaking the user experience. It doesn't mean "add a ton of code for old browsers just to replicate the visual appearance of the element" because that code will be downloaded even by modern browsers, but never used.

Conclusion

HTML is accessible and semantic by default and CSS provides handy dynamic ways to build our interfaces. If you add some custom properties you can have a really smart component without using javascript or other libs to achieve simple tasks like this one.

Enjoy it! 🎉