Standardizing Focus Styles With CSS Custom Properties

Take two minutes right now and visit your current project in a browser. Then, using only the Tab key, you should be able to navigate between interactive elements including buttons, links, and form elements.

If you are sighted, you should be able to visually follow the focus as it jumps between elements in the DOM. But if you do not see any visual change, or only a barely noticeable visual change, then you’ve found the one thing you can do to make a big difference for your visitors.

We’re going to look at a technique to make your focus styles more manageable across your project by using CSS custom properties and learn about a modern CSS focus selector. But first, let’s learn more about why visible focus styles are important.

Meeting WCAG Focus Style Criteria

Visible focus states are covered in the Web Content Accessibility Guidelines (WCAG) Success Criterion 2.4.7 – Focus Visible. The Understanding doc for 2.4.7 states the following in the intent of this criteria:

The purpose of this success criterion is to help a person know which element has the keyboard focus. It must be possible for a person to know which element among multiple elements has the keyboard focus.

In the upcoming WCAG 2.2, a new criterion is being added to clarify “how visible the focus indicator should be.” While currently in draft, getting familiar with and applying the guidelines in 2.4.11 – Focus Appearance (Minimum) is definitely a positive step you can take today to improve your focus styles.

Managing focus style with CSS custom properties

A technique I’ve started using this year is to include the following setup early in my cascade on the primary base interactive elements:

:is(a, button, input, textarea, summary) { --outline-size: max(2px, 0.08em); --outline-style: solid; --outline-color: currentColor;
} :is(a, button, input, textarea, summary):focus { outline: var(--outline-size) var(--outline-style) var(--outline-color); outline-offset: var(--outline-offset, var(--outline-size));
}

This attaches custom properties that allow you the flexibility to customize just parts of the outline style as needed to ensure the focus remains visible as the element’s context changes.

For --outline-size, we’re using max() to ensure at least a value of 2px, while allowing the possibility of scaling relative to the component (ex. a large button or link within a headline) based on 0.08em.

A property you might not be familiar with here is outline-offset which defines the space between the element and the outline. You can even provide a negative number to inset the outline, which can be very useful for ensuring contrast of the focus style. In our rule set, we’ve set that property to accept an optional custom property of --outline-offset so that it can be customized if needed, but otherwise it has the fallback to match the --outline-size.

Improvements for outline appearance

Over my career, I’ve both been asked to remove outlines and removed them myself because they were considered “ugly”.

There are now two reasons outline should absolutely never have cause to be removed (in addition to the accessibility impact):

  1. outline now follows border-radius in Chromium and Firefox! 🎉 This means you can considering removing any hacks you may have used, such as faking it with a box-shadow (which has another positive accessibility impact of ensuring focus styles aren’t removed for Windows High Contrast Theme users).
  2. Using :focus-visible we can ask the browser to use heuristics to only show focus styles when it detects input modalities that require visible focus. Simplified, that means mouse users won’t see them on click, keyboard users will still have them on tab.

It’s important to note that form elements always show a focus style — they are exempt from the behavior of :focus-visible.

So let’s enhance our rule set to add the following to include :focus-visible. We’ll keep the initial :focus style we already defined for older browsers so that it’s not lost just in case.

:is(a, button, input, textarea, summary):focus-visible { outline: var(--outline-size) var(--outline-style) var(--outline-color); outline-offset: var(--outline-offset, var(--outline-size));
}

Due to the way browsers throw out selectors they don’t understand, we do need to make these separate rules and not combine them even though they define the same outline properties.

Finally, we also need this kind of funny-looking :focus:not(:focus-visible) rule that removes the regular focus styles for browsers that support :focus-visible:

:is(a, button, input, textarea, summary):focus:not(:focus-visible) { outline: none;
}

Of note is that the latest versions of Chromium and Firefox have switched to using :focus-visible as the default way to apply focus styles on interactive elements, and just recently was enabled as default in webkit so it should be in Safari stable soon! Our rules are still valid since we’re customizing the outline appearance.

For more guidance on visible focus styles, I recommend Sara Soueidan’s amazing and thorough guide to focus indicators because it considers the upcoming 2.4.11 criterion.

Focus styles demo

This Pen shows examples of each of these interactive elements and how to apply customizations using the custom properties, including a few swaps for dark mode. Depending on your browser support, you may not see a focus style due to :focus-visible unless you use the tab key.

One final note: button is a unique interactive element when it comes to focus styles because it has additional considerations across its states, particularly if you are relying on color alone. For help with that, try out the palette generator from my project ButtonBuddy.dev.