How to Progressively Enhance Hover and Focus Styles

A side view image of a hummingbird hovering in front of a purple flower
Li Zilles

UX Developer

Li Zilles

Let’s say you’re a UXD developer and you’ve received this design comp: There’s a card component with an associated toolbar, and the comp specifies that the toolbar should be visible when the user hovers over the card. To wit:

This implementation completely excludes users navigating by keyboard. There are several different approaches to making this “hover card” more accessible, such as changing the HTML structure or introducing JavaScript.

In this post, we’ll focus on modern CSS techniques to progressively enhance our card, keeping it accessible to keyboard users while also satisfying our design stakeholders. By using feature queries and progressive enhancement, we can provide the best-supported fallback to older browsers that may not support a modern selector. This way, we can apply our enhancements with confidence that they will not break UX on older browsers.

Complementing :hover with :focus-within

The humble :focus selector has been with us for as long as :hover, but always suffered from the limitation of only being able to style the exact element that is focused. While multiple elements can be hovered at once–allowing us to set a :hover state on a parent container–only one element at a time will ever have a :focus state.

In the mid-2010s, browsers started offering support for a new selector: :focus-within. Where :focus can only style the exact element that is focused, :focus-within can style a parent element that contains a focused element anywhere inside of it. By combining this with our hover style, our toolbar now displays with keyboard navigation!

This is great, but it introduces a new and perhaps unexpected behavior for mouse users: If we hover with a pointing device, click on a button, and then move the mouse away, our toolbar is still active. What’s the deal?!

The answer: Clicking a <button> will… focus it. This correctly activates our new :focus-within style, but can we get our original behavior with :hover back?

A Side Journey: from :focus to :focus-visible

By default, browsers ship with a default focus style, typically some sort of outline around the focused element. This outline is (ideally) highly visible, as its purpose is to help visually orient the user on the web page. Since clicking on a focusable element, such as a link or button, focuses that element, an outline will – by default – show up on these elements even when a user is navigating with a pointing device.

Over the years, an unfortunate anti-pattern among web developers emerged: removing focus styles entirely. This makes it much harder for someone using keyboard navigation to tell where they are on a page; it is essentially aesthetics at the cost of accessibility.

And thus, the :focus-visible selector was born – that is, expressly introduced in order to “visibly indicate the focus only when it would be most helpful to the user”. What determines “most helpful to the user” is heuristics that are implemented by the browser itself, such as the user’s input modality – i.e., whether they are interacting via mouse or keyboard. The working draft ends the :focus-visible section with:

User agents should also use :focus-visible to specify the default focus style, so that authors using :focus-visible will not also need to disable the default :focus style.

Amazing!

A New Contender Arrives: :has()

So far, we’ve discussed the variants and flavors of :focus that have developed over the years. Now I’d like to talk about an exciting new selector that ostensibly has nothing to do with focus: the :has() selector.

Also known as the “parent selector”, here’s how :has() works: You put another selector inside the parentheses, and it will match any container that “has” that selector inside of it. Let me say that again: :has([selector]) can style a parent element that contains an element that matches[selector] anywhere inside of it. Sound familiar? Maybe like a more generalized version of a certain :focus-within?

Yes, that’s right – :has(:focus) is effectively the same as :focus-within (although there are subtle differences to watch out for with shadow DOM). And while neither :focus-within nor :has(:focus) will work for exactly the behavior we’re looking for, it turns out that :has(:focus-visible) is just the ticket.

We now have a card that:

  • Displays a toolbar when hovered, or when any button is focused with the keyboard
  • Does not display the toolbar when the mouse is not hovering, after a button is clicked

We’ve preserved the design and the accessibility! Hooray! 🎉

Conclusion

In the past, we would have needed JavaScript to support these styles. No longer!

When it comes to hover and focus styles, here are my recommendations:

  • :hover styles should always be accompanied by a corresponding :focus or :focus-within
  • :focus can be enhanced with :focus-visible
  • :focus-within styles can be enhanced with :has(:focus-visible)

Ideally, design comps would come with explicit guidance on focus styles, but unfortunately, this isn’t always the case.

When this guidance isn’t available, I tend to view focus similar to a cursor – it’s just a slightly different way of representing where the user is currently interacting on the page. If style is applied on hover, then it generally should be applied on focus as well.

This is not to say that these styles should be equivalent, as the focus state should still be distinct from hover – so make sure to augment those styles with some ✨ focus flair ✨ as well! (Maybe go for a highly visible outline rather than sparkles, though.)

While we’ve enhanced our card component with better hover and focus styles, there are still some issues. For instance, how does this component work on touchscreens, where neither mouse nor keyboard interaction is the norm? What do the ARIA roles and attributes look like for this widget, if they are necessary at all?

While these concerns are outside the scope of this blog post, they’re extremely important for UX developers to consider when implementing interfaces accessible to all users.

Now more than ever, accessibility should never need to be sacrificed for aesthetics. (Build accessibility into design!). With modern CSS, we have more tools than ever for styling accessible interfaces while still meeting the design expectations of other product stakeholders. Let’s use them!

From the first design steps through UXD and production, DockYard keeps accessiblity top of mind throughout the development process. Get in touch today to learn how you can benefit from our holistic approach to digital product development.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box