Focus inside Shadow DOM

Sam Thorogood
Feb 21, 2017 · 5 min read

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.

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… ⁉️

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.

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 —

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

Dev Channel

Developers Channel - the thoughts, opinions and musings…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store