Whistlr
@samthor

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.

Demonstration of a 'Hello, My Name Is' element
namecard demo, inspired by the original Chrome 25 blogpost

There's a few moving parts here, so read them before you continue:

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.

Demonstration of changing the current activeElement including inside a Shadow Root
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. Save document.activeElement as the work element

  2. If the work element has a shadow root, then;

    1. Save its shadowRoot.activeElement as the work element, if non-null

    2. 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.

Chrome's built-in <input type='datetime'>
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

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.

An example of a Shadow Root with two children <input> elements

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:

  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;

    1. Add a focus handler to the working SR

    2. 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 📡.

Originally posted on Medium.