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.
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
selector but applying also to ancestors,
so somehow working like
If you see an example it’s pretty simple to understand:
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
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.
An element also matches
:focus-withinif one of its shadow-including descendants matches
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:
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:
The issue here is that when you focus the input, as it’s now inside
<slot> tag, you’d expect that the
shadowDiv has a green border.
However, the input is not a shadow-including descendant
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-withinif one of its descendants in the flat tree (including non-element nodes, such as text nodes) matches the conditions for matching
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:
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).
During the review Rune found another controversial topic.
The question is what happens to a focused element when it’s marked
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.
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
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:
What happens when you move the focus from
Let’s see this step by step with the initial patch:
input1is focused, so this element and all its ancestors have the
:focus-withinflag (all of them will have a green border), that includes
<html>but let’s ignore that for this explanation).
- 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
input2is actually focused, and we go again through the ancestors chain adding the flag to
As you see we’re removing and adding the flag from
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
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
li1 get the flag removed,
and in point (3) only
li2 get it added. The other elements
<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.
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.
This new selector has an important impact on making the Web more accessible,
especially to keyboard users. For example, if you only use
leaving out a chunk of your user base, the ones using keyboard navigation,
but now you could easily combine that with
avoiding this kind of problems.
Again I’ve crafted a typical menu using
take a look to how keyboard navigation works.
Note that there’s a Firefox bug preventing this last example to work there.
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.