Enhancing focus visibility - focus-within or has(:focus)?
Previously we have seen how to style an element in focus mode with CSS :focus
and :focus-visible
pseudo-classes for better accessibility. What if we want to style a parent element when its child is on focus and make it more standout for the user's visibility on a crowded page? What are the options for us to achieve this?
Let's find out in this article, starting with the :focus-within
pseudo-class.
Table of Content
- Table of Content
- Using focus-within pseudo-class
- Using has() pseudo-class for focus within
- When to use focus-within and has(:focus)?
- Summary
Using focus-within pseudo-class
While the :focus
pseudo-class is for selecting an element in focus mode, the :focus-within
is for selecting that element's ancestor in the DOM tree. In short, :focus-within
will match any element on which one of its nesting elements is focused (matching :focus
).
Let's look at the following example of a search box where we have two levels of nesting. The first level is the div
with a class of search-container
, and the second is the label
element.
<div class="search-container">
<label>
Search a title
<input
type="text"
placeholder="Search"
id="search-box"
/>
</label>
<div>
<button class="clear-btn">Clear</button>
<button class="search-btn">Search</button>
</div>
</div>
We then add some outline styles to :focus-within
of the top level div
with the class search-container
, and font's boldness to :focus-within
of the label
element, as follows:
.search-container:focus-within {
outline: 2px solid rgba(66, 153, 225, 0.5);
outline-offset: 5px;
}
label:focus-within {
font-weight: 600;
}
With that, when we focus on the input
element within label
, the label text becomes bold, and the outline of the top level div
with class search-container
becomes visible. And when we focus on the buttons, either by clicking on them or by using the Tab key, we will see that only the top-level div
with class search-container
changes its outline style:
To debug the styles for :focus-within
, we can head to the DevTools, inspect the desired element and toggle the element's :focus-within
state listed under the Styles
tab - :hov
section. We can also toggle the :focus
state of its children following the same approach and see how the parent's styles change:
Now that we understand the :focus-within
pseudo-class, let's see if we can achieve the same result with the :has()
pseudo-class next.
Using has() pseudo-class for focus within
The :has()
is a functional pseudo-class that accepts a list of relative selectors as input. It allows us to select and style an element if that element has a child (or a descendant) that matches the given list of selectors using the following syntax:
:has(<selectors-list>) {
/* styles */
}
For example, using our previous search box, we want to style the top level div
with class search-container
if it has a child element that matches the following conditions:
- The child has a class name,
clear-btn
and - The child is focus mode
We can use :has()
pseudo-class to achieve the above task as follows:
.search-container:has(.clear-btn:focus) {
outline: 2px solid rgba(66, 153, 225, 0.5);
outline-offset: 5px;
}
And that's all it takes. Now only when we focus on the Clear
button will we see the top level div
with class search-container
changes its outline style, leaving the rest of the elements' styles untouched:
Additionally, we can also pass multiple selectors to :has()
pseudo-class, such as targeting the focus state of both the Clear
button and the Search
button:
.search-container:has(.clear-btn:focus, .search-btn:focus) {
outline: 2px solid rgba(66, 153, 225, 0.5);
outline-offset: 5px;
}
Great. So far, we have seen how we can style a parent element based on the focus state of its children using :focus-within
and :has()
pseudo-classes. Here comes the question, which one should we use and when?
When to use focus-within and has(:focus)?
Styling a parent element when one of its children is in focus can significantly improve visibility besides :focus
and :focus-within
. When navigating with a keyboard in a long list of components, such as a list of product cards, articles, etc., adding extra style to the parent element can help users quickly identify the focused part and its container, hence understanding the context better.
Generally, using :focus-within
pseudo-class is the most straightforward way to achieve the above task. However, :focus-within
matches the focus state of any of its descendants, which means that if we have multiple descendants in focus mode, the ancestor will also change its styles. And this is not always the desired behavior.
Take our search box, for instance. When the search input is on focus, we may only want to highlight the label of the search box but not add any extra style to the top level div
. And when one of the two buttons Clear and Search is in focus, we highlight the container div
. With just :focus-within
, it is impossible to achieve both scenarios.
In such cases, using :has(:focus)
on a specific container or adding an explicit selector to :has()
like :has(#search-box:focus)
can be a better solution.
However, there is a performance catch here. Depending on how complex the relative selectors passed to it, :has()
can be costly as the CSS engine needs to query all the matching elements for style calculation. In general, :focus-within()
is faster than :has(:focus)
, though the difference may not be critical. Below are the screenshots of performance tests for both pseudo-classes when adding to the same element li
within a list:
- With
:focus-within
pseudo-class
- With
:has(:focus)
pseudo-class
Though the performance difference in the above test may not be significant, it is worth keeping in mind when we have a complex DOM structure and whether we need to specify a children's focus state or a regular "catch them all" like :focus-within
will be enough.
Lastly, the :has()
functional pseudo-class is not supported in Firefox yet, so you may want to use :focus-within
or add a fallback instead.
Summary
In this article, we have learned how to use two pseudo-classes, :focus-within
and :has()
, to style a parent element based on the focus state of its children. We have also seen the difference between them and when one can be a more suitable choice than the other. While the accessibility impact can sometimes be subtle and not obvious, like direct focus style, it is always an excellent consideration to add a bit of visibility to the parent element, like a list item with interactive components inside. Users with keyboard navigation will appreciate you for that.
๐ Learn about Vue with my new book Learning Vue. The early release is available now!
๐ If you'd like to catch up with me sometimes, follow me on Twitter | Facebook | Threads.
Like this post or find it helpful? Share it ๐๐ผ ๐