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 output of our icon button element:

  • Interactive
  • Keyboard controlled
  • Accessible
  • Customizable

...and we will achieve all of these 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 type="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 type="button">
<span class="sr-only">Download File</span>

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-accessible.

  • Interactive
  • Keyboard controlled
  • Accessible
  • Customizable


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 transparent 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 obfuscate the background layer.

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

.IconButton {
all: unset;
width: 56px;
height: 56px;
border-radius: 50%;
background-color: #000;
cursor: pointer;
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 {

/* Icon 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 losing quality. We will use this icon for this example.

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

.IconButton::before {

/* Previous code... */

/* Masking code */
mask-image: url('');
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;

As you can see we defined four things here:

  • The mask url: our black svg icon
  • The mask position: set to be always centered
  • The mask repetition: we disabled the tile effect.
  • The mask size: we set it to be contained into the available space, so the icon will be always fully visible.

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 a 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:


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:


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:"--icon", "...");

We can now flag the last step! 🎉

  • Interactive
  • Keyboard controlled
  • Accessible
  • Customizable

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 don'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.


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! 🎉