Similar to what I wrote for caret-color in January, this is a blog post about the process to implement a new feature on Chromium/Blink. This time it’s the turn for :focus-within pseudo-class from the Selectors 4 spec, I’ll talk about the different things that happened during the development.

:focus-within pseudo-class

This is a new selector that allows to modify the style of an element when this element or any of its descendants are focused. It’s similar to the :focus selector but applying also to ancestors, so somehow working like :active and :hover.

If you see an example it’s pretty simple to understand:

<style>
  form:focus-within {
    background-color: green;
  }
</style>
<form>
  <input />
</form>

In this example, when the input is focused the form background will switch to green.

Intent to ship

Although the specification is still in the Editor’s Draft (ED) state, it has already been implemented in Firefox 52 and Safari 10.1, so it seems like a good candidate to be added to Chromium too.

For that you need to send an intent mail to blink-dev. This seemed like something small and simple enough and, after investigating a little bit about the feature, I decided to send the mail: Intent to Implement and Ship: CSS Selectors Level 4: :focus-within pseudo-class.

But here the first problems arose…

Issues on the spec

On a first sight you can think that this is a very simple feature, but the Web Platform is complex and has many things interacting between each other.

In this case Rune Lillesveen promptly detected an issue on the spec text, related to the usage of this selector (and also :active and :hover) with Shadow DOM. The old text from the spec said:

An element also matches :focus-within if one of its shadow-including descendants matches :focus.

It seems the spec was ready regarding Shadow DOM, but it was not right. This can be quite tricky to understand but if you’re interested take a look to the following example:

<div id="shadowHost">
  <input />
</div>
<script>
  shadowHost.attachShadow({ mode: "open"}).innerHTML =
    "<style>" +
    "  #shadowDiv:focus-within { border: thick solid green; }" +
    "</style>" +
    "<div id='shadowDiv'>" +
    "  <slot></slot>" +
    "</div>";
</script>

Just in case you don’t understand this example, the final result is that the input element gets inserted into the <slot> tag (this is just a quick and dirty explanation about this particular Shadow DOM example).

The flat tree for this example would be something like this:

<div id="shadowHost">
  #shadow-root
  <div id="shadowDiv">
    <slot>
      <input />
    </slot>
  </div>
</div>

The issue here is that when you focus the input, as it’s now inside the <slot> tag, you’d expect that the shadowDiv has a green border. However, the input is not a shadow-including descendant of the shadowDiv. The spec should talk about the descendants in the flat tree instead.

The issue was reported to the CSS WG GitHub repository and fixed using the following prose:

An element also matches :focus-within if one of its descendants in the flat tree (including non-element nodes, such as text nodes) matches the conditions for matching :focus.

Implementing :focus-within

Once the spec issue got resolved, the intent was approved. So I had green light to move forward on the implementation.

The patch to support it was mostly boilerplate code required to add a new selector on Blink. Most of it was doing something very similar to what :focus already does, but then we have the interesting part, a loop through the ancestors of the element using the flat tree:

for (ContainerNode* node = this; node;
     node = FlatTreeTraversal::Parent(*node)) {
  node->SetHasFocusWithin(received);
  node->FocusWithinStateChanged();
}

What about tests?

Of course you need tests for any change on Blink, in this case I was lucky enough as the W3C Web Platform Tests (WPT) repository already have a few tests for this new selector.

I imported these tests (not without some unrelated issues) into Blink and verified that my patch passed them (including Mozilla tests that were already upstreamed). On top of that, I checked the tests in WebKit repository, as they have already implemented the feature and upstreamed one of them that was checking some nice combinations. And finally, I also wrote a few more tests to cover more situations (like the spec issue described above).

Focus and display:none

During the review Rune found another controversial topic. The question is what happens to a focused element when it’s marked as display: none. At first glance, you would think that the element should lose focus, and you’ll be right (HTML spec has a rule specifically covering this case).

But here we have to deal with an interoperability issue, because the only engine currently following this rule is Blink. There are bug reports in the rest of the browsers, and they seem to acknowledge the issue but there is no activity to fix this at this point. If you are interested in more details, all of them are linked from Chromium bug #491828.

If you’re using :focus selector to change, for example, the background of an input, it’s not very important what happens when that input gets display: none and dissapears. You don’t care about the background of something that you’re not seing anymore. However, with focus-within this issue is more noticeable. Imagine that you’re changing the background of a form when any of its inputs is focused. If the focused input is marked with display: none, you won’t have anything focused in the form so its background should change, but that only happens in Chromium right now.

Common ancestor strategy

The initial patch supporting :focus-within landed in time for Chrome 59, but it was implemented behind a experimental flag. The main reason was that it still needed some extra work before being ready to be enabled by default.

One of those things was related to style recalculations, the initial implementation was causing more recalculations than required.

Let’s use a new example:

<style>
  *:focus-within {
    background-color: green;
  }
</style>
<form>
  <ul>
    <li id="li1"><input id="input1" /></li>
    <li id="li2"><input id="input2" /></li>
  </ul>
</form>

What happens when you move the focus from input1 to input2?

Let’s see this step by step with the initial patch:

  1. Initially input1 is focused, so this element and all its ancestors have the :focus-within flag (all of them will have a green border), that includes input1, li1, <ul> and <form> (actually even <body> and <html> but let’s ignore that for this explanation).
  2. Then when we move to input2, the first thing is that the previous focused element, in this case input1, loses the focus. And at that point we go through the ancestors chain removing the :focus-within flag from input1, li1, <ul> and <form>.
  3. Now input2 is actually focused, and we go again through the ancestors chain adding the flag to input2, li2, <ul> and <form>.

As you see we’re removing and adding the flag from <form> and <ul> elements when it’s not actually needed as they end up in the same status.

What the new version changes is that in point (2) it looks for the common ancestor between the element losing the focus and the one gaining it. In this case the common ancestor between input1 to input2 would be the <ul>. So when walking the ancestor chain to add/remove the :focus-within flag, it stops in the common ancestor and let it (and all its ancestors) unmodified. This way we’re saving style recalculations.

Now in point (2) only input1 and li1 get the flag removed, and in point (3) only input2 and li2 get it added. The other elements <ul> and <form> remain untouched.

And even more things…

Taking advantage of this work on Chromium, I realized that WebKit was not following the spec in the flat tree case. So I imported the WPT tests into WebKit and make a one liner patch to use the flat tree in WebKit too.

Adding a new selector might seem a simple task, but let me show you some numbers about the commits on the different repos related to all this work:

And a few more might come as I’m still doing a few modifications on the tests so we can use them in both Blink and WebKit without issues.

Use cases

Now everything has landed and :focus-within will be available by default starting in Chrome 60. So it’s time to start using it.

I’ve created a simple demo about what you can do with it, but probably you can think of much cooler stuff.

:focus-within demo

This new selector has an important impact on making the Web more accessible, especially to keyboard users. For example, if you only use :hover you’re leaving out a chunk of your user base, the ones using keyboard navigation, but now you could easily combine that with :focus-within avoiding this kind of problems.

Again I’ve crafted a typical menu using :hover and :focus-within, take a look to how keyboard navigation works.

Use keyboard navigation on a :focus-within menu

Note that there’s a Firefox bug preventing this last example to work there.

Thanks!

As usual I’ll finish the post with the acknowledgements section. The development of this new pseudo-class has been done by Igalia sponsored by Bloomberg as part of our ongoing collaboration.

Igalia and Bloomberg working together to build a better web Igalia and Bloomberg working together to build a better web

On top of that I have to thank Florian Rioval for helping with the tests reviews on WPT. And especially to Rune Lillesveen for all his work and help during the whole process.