:focus-visible in WebKit - January 2021
Let’s do a small introduction as this is a kind of special post.
As you might already know, last summer Igalia launched the Open Prioritization experiment, and :focus-visible
in WebKit was the winner according to the pledges that the different projects got. Now it has moved into collecting funds stage, so far we’ve reached 80% of the goal and Igalia has already started to work on this. If you are interested and want to help sponsoring this work, please visit the project page at Open Collective.
In our regular client projects in Igalia, we provide periodic progress reports about the status of tasks and next plans. This blog post is a kind of monthly report, but this time the project has many customers, so it looks like this is a better format to share information about the status of things. Thank you all for supporting us in this development! 🙏
Understanding :focus-visible
#
Disclaimer: This is not a blog post explaining how :focus-visible
works or the implications it has, you can read other articles if you’re looking for that.
First things first, my initial thoughts were that :focus-visible
was a pseduo-class which would match an element when the browser natively shows a focus indicator (focus ring, outline) when an element of a page is focused. And that’s more or less what the spec says on the first sentence:
The
:focus-visible
pseudo-class applies while an element matches the:focus
pseudo-class and the user agent determines via heuristics that the focus should be made evident on the element.
They key part here is that native behavior doesn’t show the focus indicator on purpose in some situations when the :focus
pseudo-class matches, mainly because usability studies indicate that showing it in all the cases is not what the user expects and wants. Before having :focus-visible
the web authors have not way to access the same criteria to style the focus indicator only when it’s going to be shown natively, and still keep the website being accessible.
Apart from that the spec has a set of heuristics that despite being non-normative, it looks like all implementations are following them. Summarizing them briefly they’d be something like:
- If you use the mouse to focus an element it won’t match
:focus-visible
. - If you use the keyboard it’ll match
:focus-visible
. - Elements that support keyboard input (like
<input>
orcontenteditable
) always match:focus-visible
. - When a script focuses a new element it’ll match or not
:focus-visible
depending on the previous active element.
This is just a quick & dirty summary, please read the spec for all the details. There have been years of research around these topics (how focus should work or not on the different use cases, what are the users and accessibility needs, how websites are managing focus, etc.) and these heuristics are somehow the result of all that work.
:focus-visible
in the default UA style sheet #
At this point it looks like we can more or less understand what :focus-visible
is about. So let’s start playing with it. The definition seems very clear, but testing things in the current implementations (Chromium and Firefox) you might find some unexpected situations.
Let’s use a very basic example:
<style>
:focus-visible { background: lime; }
</style>
<div tabindex="0">Focus me.</div>
:focus-visible
is not supported on your browser.If you focus the <div>
with a mouse click, :focus-visible
doesn’t match per spec, so in this case the background doesn’t become green (if you use the keyboard to focus it will match :focus-visible
and the background would be green). This works the same in Chromium and Firefox, but Chromium (despite the element doesn’t match :focus-visible
) shows a focus indicator. Somehow the first spec definition is already not working as expected on Chromium… The issue here is that Chromium still uses :focus { outline: auto; }
in the default UA style sheet, and the element matches :focus
after the mouse click, that’s why it’s showing a focus indicator while not matching :focus-visible
.
Actually this was already on the spec, but Chromium is not following that yet:
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.
There was already a related CSSWG issue on the topic, as the spec currently suggests the following code:
:focus:not(:focus-visible) {
outline: 0;
}
This works as a kind of workaround for this issue, but if the default UA style sheet uses :focus-visible
that won’t be needed.
Anyway, I’ve reported the Chromium bug and created WPT tests, during the tests review Emilio Cobos realized that this needed a change on the HTML spec and he wrote a PR to update it. After some discussion with Alice Boxhall the HTML change and the tests were approved and merged. I even was brave enough to write a patch to change this in Chromium which is still under review and needs some extra work.
The tests #
WebKit is the third browser engine adding support for :focus-visible
so the first natural step was to import the WPT tests related to the feature. This looked like something simple but it ended up needing some work to improve the tests.
There was a bunch of :focus-visible
tests already in the WPT suite, but they need some love:
- Some parts of the spec were not covered by tests, so I added some new tests.
- Some tests were passing in WebKit, even when there was not an implementation yet, so I modified them to fail if there’s no
:focus-visible
support.
Then I imported the tests in WebKit and I discovered a bug related to focus
event and :focus
pseudo-class. :focus
pseudo-class was not matching inside the focus
event handler. This is probably not important for web authors, but :focus-visbile
tests were relying on that. Actually this had been fixed in Chromium more than 5 years ago, so first I moved to WPT the Chromium internal test and used it to fix the problem in WebKit.
Once the tests were imported in WebKit, the problem was that a bunch of them were timing out in Mac platforms. After investigating the issue I realized that it’s because focus
event is not dispatched when you click on a button in Mac. Digging deeper I found this old bug with lots of discussions on the topic, it looks like this is done to keep alignment with the native platform behavior, and also in order to avoid showing a focus indicator. Even Firefox has the same behavior on Mac. However, Chromium always dispatch the event independently of the platform. This makes that some of the tests don’t work automatically on Mac, as they wait for a focus
event that is never dispatched. Anyway maybe once :focus-visible
is implemented, it could be rediscussed the possibility of modifying this behavior, thought it might be not possible anyway. In any case, WebKitGTK port, the one I’m using for the development, does trigger the focus
event in this case; and I’ve also changed WPE port to do the same (maybe Windows port will follow too).
One more thing about the tests, lots of these :focus-visible
tests use testdriver.js
to simulate user actions. For example for clicking an element they use test_driver.click(element)
, however that simple instruction is causing some kind of memory leak on Chromium when running the tests. The actual Chromium bug hasn’t been fixed yet, but I landed some workarounds that prevent the issue in these tests (waiting for the promise to be resolved before marking the test as done).
To close the tests part, you can check the status in wpt.fyi, most of them are passing in all implementations which is great, but there are some interoperability issues that we’ll review next.
Interop issues #
As I mentioned the wpt.fyi website helps to easily identify the interop issues between the different implementations.
-
:focus-visible
on the default UA style sheet: This has been already commented before, but this is the reason why Chromium failsfocus-visible-018.html
test. Firefox failsfocus-visible-017.html
because the default UA style sheet mentionsoutline: auto
, but Firefox uses adotted
outline. -
:focus-visible
on<select>
element: There’s a Firefox failure onfocus-visible-002.html
because it doesn’t match:focus-visible
when you click a<select>
element. I opened a CSSWG issue to discuss this, and I initially thought that the agreement was that Firefox behavior is the right one. So I did a patch to change Chromium’s behavior and update the tests, but during the review I was pointed to a Chromium bug about this topic that was closed as WONTFIX, the reason is that when you click a<select>
element you can type letters to select the option from the keyboard. Right now the discussion has been reopened and we’ll need to wait for the final resolution on the topic, to see which is the right implementation. -
Keyboard interaction once an element is focused: This is tested by
focus-visible-007.html
. The example here is that you click an element to focus it, initially the element doesn’t match:focus-visible
but then you use the keyboard (for example you type a letter), in that situation Chromium will start matching:focus-visible
while Firefox won’t. The spec is quite explicit on the topic so it looks like a Firefox bug:If the user interacts with the page via the keyboard, the currently focused element should match
:focus-visible
(i.e. keyboard usage may change whether this pseudo-class matches even if it doesn’t affect:focus
). -
Programmatic focus and
:focus-visible
: What should happen with:focus-visible
when the website useselement.focus()
from a script to move the focus? The spec has some heuristics that depend on if the active element beforefocus()
is called was matching (or not):focus-visible
. But I’ve opened a CSSWG issue to discuss what should happen when there’s no active element. The discussion is still ongoing and depending on that there might be changes in the current implementations. Right now there are some subtle differences between Chromium and Firefox here.
:-webkit-direct-focus
? #
Probably you don’t know what’s that, but it’s somehow related to :focus-visible
so I believe it’s worth to mention it here.
WebKit is the browser that supports better :focus
pseudo-class behavior on Shadow DOM (see the WPT tests results). The issue here is that the ShadowRoot
should match :focus
if some of the descendants are focused, so if you have an <input>
element in the Shadow Tree, and you focus it, you’ll have 2 elements matching :focus
the ShadowRoot
and the <input>
.
<style>
#host { padding: 1em; background: lightgrey; }
#host:focus { background: lime; }
</style>
<div id="host"></div>
<script>
shadowRoot = host.attachShadow(
{mode: 'open', delegatesFocus: true});
shadowRoot.innerHTML =
'<input value="Focus me">';
</script>
In Chromium if you use delegatesFocus=true
in element.attachShadow()
, and you have an example like the one described above, you’ll get two focus indicators, one in the ShadowRoot
and one in the <input>
. Firefox doesn’t match :focus
in the ShadowRoot
so the issue is not present there.
WebKit matches :focus
independently of delegatesFocus
value (which is the right behavior per spec), so it’d be even more common to have a situation of getting two focus indicators. To avoid that WebKit introduced :-webkit-direct-focus
pseudo-class, that is not web exposed, but it’s used in the default UA style sheet to avoid this bad effect of having a focus indicator on the ShadowRoot
.
I believe :focus-visible
spec should describe that behavior regarding how it works on ShadowRoot
so it doesn’t match on those situations. That way WebKit could get rid of :-webkit-direct-focus
and use :focus-visible
once it’s implemented. I’ve reported a CSSWG issue to discuss this topic.
WIP implementation #
So far I haven’t talked about the implementation at all, but the reason is that all the previous work is required in order to be able to do a proper implementation, with good quality and that is interoperable between the different browsers. :focus-visbile
is a new feature, and despite all the interop mess regarding how focus works in the different browsers and platforms, we should aim to have a :focus-visible
implementation as much interoperable as possible.
Despite all this related work, I’ve also found some time to work on a patch. It’s still not ready to be sent upstream but it’s already doing some things and passing some of the WPT tests. Of course several things are still missing, but next you can see quick screen recording with :focus-visible
working on WebKit.
Some numbers #
I know this is not really relevant, but it helps to get a grasp on what has been happening during this month:
- 3 CSSWG issues reported.
- 13 PRs merged in WPT.
- 5 patches landed in WebKit.
- 4 patches landed in Chromium.
- And many discussions with different people, special thanks to Alice and Emilio that have been really helpful.
Next steps #
The plan for February is to try to find an agreement on the CSSWG issues, close them, and update the WPT tests accordingly. Maybe this work could include even landing some patches on the current implementations. And of course, focus (pun intended) the effort on implementation of :focus-visible
in WebKit.
I hope this blog post helps you to understand better the work that goes behind the scenes when a web platform feature is implemented, especially if you want to do it on a way that ensures browser interoperability and reduces web authors’ pain.
If you enjoyed this project update, stay tuned as there will more in the future.
- Previous: 2020 Recap
- Next: :focus-visible in WebKit - February 2021