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):
outline
now followsborder-radius
in Chromium and Firefox! 🎉 This means you can considering removing any hacks you may have used, such as faking it with abox-shadow
(which has another positive accessibility impact of ensuring focus styles aren’t removed for Windows High Contrast Theme users).- 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.
I usually keep the focus style for mouse users on click as well as it can be very useful for quickly finding where you were after navigating back or closing a tab, especially if a link/button were in a longer list.
I think I need a whole tutorial on how best to have my simple websites be accessible, intuitive, and somewhat consistent between smart phone, mouse-, track-pad-, and keyboard-inputs without having to code, test, and maintain for each separately. My head hurts thinking about focus, tabs, hovering, and right-clicking across all those environments. I still have so much to learn!
This is the code that I’m using on my personal website which I thought I’d share too:
Personally, I find it more aesthetically pleasing.
PS. It’s actually borrowed from Discord’s web app, but I’ve improved the colour contrast for accessibility.
less typing
less file size
higher readability
otherwise you would need to add
:focus
,:focus-visible
, … to each HTML tag selectorCurious, why:
:is(a, button, input, textarea, summary) {...}
and not just:
a, button, input, textarea, summary {...}
Thanks!
Good question, Aaron — it’s most likely to do with specificity.
:is
takes the specificity of its strongest (i.e. most specific) argument. That makes things tougher to override by accident later in the cascade.Since all of the arguments in this example share the same specificity, I don’t think there’s any benefit to using :is here.
I find adding the not:(focus-visible) rule removes the previously added focus styles in Firefox Developer edition 96.0b3, not just browser defaults.
But a really helpful article otherwise.
One important note: you have to opt in to the “tab to focus links” behavior on Firefox; by default, the tab key skips links.
I always have to look up how because it’s kind of obtuse.
https://stackoverflow.com/questions/11704828/how-to-allow-keyboard-focus-of-links-in-firefox
Keep an eye out for ms high contrast mode and consider something like following:
And also Safari’s lack of
focus-visible
support.Since Safari doesn’t currently support
:focus-visible
it may make sense to have a graceful degradation where we pointfocus
to use thefocusVisible
styles (imagine thatfocusVisible
is a mixin in SCSS world)