cssaccessibilitywebdevtutorial

Enhancing focus visibility - focus-within or has(:focus)?

Aug 08, 2023 ยท 6 min read
Buy Me A Coffee
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

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:

Different styles when focusing on input, and when focusing on buttons

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:

Toggle focus-within state in the dev tools

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:

Changing outline of the parent on button focused only

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.

Changing outline of the parent on button focused only

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
Toggle focus-within state in the dev tools
  • With :has(:focus) pseudo-class
Toggle focus-within state in the dev tools

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 ๐Ÿ‘‡๐Ÿผ ๐Ÿ˜‰

Buy Me A Coffee