Focus inside Shadow DOM

In 2017, across both Chrome and Safari (with Edge and Firefox coming soon), 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.

namecard demo, inspired by the original Chrome 25 blogpost

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”, “James”, 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.

theoretical demo with two inputs in shadow root, and one outside

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 ➡️ 📃 —

  1. Savedocument.activeElement as the work element
  2. If the work element has a shadow root, then;
    a. Save its shadowRoot.activeElement as the work element, if non-null
    b. If the work element changed, repeat from step 2
  3. 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.

inside Chrome, this date picker is written in HTML — but you can’t get to it

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—

  1. Save shadowRoot.activeElement as the work element
  2. Return the work element — if null, your element is not focused

Keen observers 🤔 will note that if your element contains further shadow roots — again, perhaps ones you don’t control — activeElement won’t pierce into them. Again, perhaps this is fine.

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', function(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’, function(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 —

  1. On ‘focus’, find the actually focused element (see “how can I find the actually focused element”)
  2. Save its shadow root as the working SR
  3. While the working SR is non-null;
    a. Add a focus handler to the working SR
    b. Find any parent shadow root and save it as the working SR
  4. 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 📡.