Focus inside Shadow DOM
Across all evergreen browsers, you can encapsulate HTML and CSS in a "shadow root". Shadow DOM allows you to keep this code separate from the rest of your document. 🙅
This is important for Web Components.
But I'll focus on Shadow DOM, and tricky logic around document.activeElement
—starting with a quick demo.
There's a few moving parts here, so read them before you continue:
-
The
div#nametag
element 📛 is known as the host, as it hosts a shadow root—it doesn't have style of its own, or need to be styled -
The shadow root (or
#shadow-root
, as Chrome refers to it) contains its own subtree of HTML nodes, styles, etc—this creates the nametag UI without placing it in the page itself -
Within the shadow root, there's a
<slot></slot>
element—any nodes in the light DOM, i.e., "Sam", "Nicky", or "someone"—are virtually positioned here
Next, it's important to know that HTML inside a shadow root isn't inert—i.e., it's not just for presentation—and it can receive user interaction.
This messes with—although it doesn't quite break—a simple invariant: that a HTML page has exactly one active element, accessible via the document.activeElement
property.
In this demo, the first two input
elements are part of a shadow root, but the last one is not—it's a regular HTML element in the light DOM (just like the "name" in the namecard above).
Even though the first two input elements receive focus,document.activeElement doesn't update—it just points to the host element—in this case, div#box
.
Ok, so how can I find the actually focused element?
It turns out that the shadow root we created on div#box
also has an activeElement
property.
So let's come up with an algorithm: ➡️📃
-
Save
document.activeElement
as the work element -
If the work element has a shadow root, then;
-
Save its
shadowRoot.activeElement
as the work element, if non-null -
If the work element changed, repeat from step 2
-
-
Return the work element
That's it! This pierces 🤺 the shadow root. However, there's another question you should ask yourself… ⁉️
Do I really need to find the actually focused element?
Well, maybe. It depends. The web isn't perfect—this technique is just for your toolbox 🔨 if you need it.
Ostensibly, the idea behind Shadow DOM is that it provides a way for Web Components to ship 🛥 ️encapsulated functionality—functionality that you shouldn't mess with. Think of them as similar to inbuilt elements—like 📆input type="date" or select, both of which render complex UI in HTML.
If you're building and shipping code yourself, then finding the focused element 🔍 within your scope is easy—you can hold onto the shadow root, so your algorithm becomes trivial:
-
Save
shadowRoot.activeElement
as the work element -
Return the work element—if null, your element is not focused
What about when the active element changes—focus and blur events?
Using the algorithms above, it's easy to find the currently active element. However, we don't get notified when it changes. Without Shadow DOM, if you wanted to know when an element became focused, you'd do this:
document.addEventListener('focus', (event) => {
console.info('new element focused', event.target);
/* do stuff */
});
That still works, but it will only ever report the host elements—this is in fact how the 2nd example, showing document.activeElement
, was built.
In this example, if I were to focus on input#one
element in the shadow root, the host will generate a focus event.
If I then tab to input#two
, then the host will do nothing—it's already focused.
However, if you control 🛠️ the shadow root, and you only need focus events within your own elements, then you can add an event handler ✋️ within the shadow root:
shadowRoot.addEventListener('focus', (event) => {
console.info('element in shadow root focused', event.target);
/* do stuff */
});
This won't scale 🔩 to everything on your page—but it is possible to leverage this technique to be notified about focus throughout all shadow roots. The approach will go something like this:
-
On
focus
, find the actually focused element (see "how can I find the actually focused element") -
Save its shadow root as the working SR
-
While the working SR is non-null;
-
Add a focus handler to the working SR
-
Find any parent shadow root and save it as the working SR
-
-
When a blur event occurs, remove all focus handlers ❌
I've hand-waved 👋 over some of the tricky parts here. But I've written a library that takes care of it for you! You can see a demo below:
Conclusion
Hopefully I've enlightened 🔦 you a bit about the way focus and the activeElement
property works with Shadow DOM.
What have I missed? I've not covered the differences between "open" and "closed" shadow roots. Google's advice in general is to use "open", and forget that "closed" exists.
Do you ever need to get access to the actual focused element?—maybe, sometimes. It's useful for writing polyfills, or hacking around the realities of the web, even if that web is futuristic 📡.
Originally posted on Medium.