DEV Community

Westbrook Johnson for Open Web Components

Posted on

Mind the `document.activeElement`!

The element that currently has focus in your HTML at any point in time can be accessed as document.activeElement. If you don't know, now you know!

What's more, while it can be difficult to capture the value of this property while debugging, at least without changing it, you can leverage browsers that allow you to "watch live expressions" to keep the current value of this property available at all times, 😱. No, really, go check it out right now!

There are lots of ways you can leverage this in your work, whether in functional code, unit tests or debugging, but I'm not looking to walk you through all the things that should be, can be, or will be in this area. However, if you're already using this value, I'd love to hear more about it in the comments. My usage can definitely be super-powered by hearing great workflows from others, so I look forward to hearing what you've got up your sleeves.

This isn't the  raw `document` endraw  you're looking for...

We are gathered here, today, to go a little bit deeper on what document means and when the document isn't the “document”0 you're looking for and what to do in that case.

Out of the shadows a new document rises...

Do you find yourself using code like the following to attach a shadow root to elements in your application?

el.attachShadow({mode: 'open'});
Enter fullscreen mode Exit fullscreen mode

Do you find yourself attaching that shadow root to custom elements that you've defined?

class CustomElement extends HTMLElement {}
customElement.define('custom-element', CustomElement);
Enter fullscreen mode Exit fullscreen mode

Then you're already using web components.

If not, I highly recommend them in many and varied use cases! The benefits I've gained from working with custom elements and shadow DOM from well before both APIs were even supported by two browsers, let alone all of them, are all positive, and the full possibilities of this sometimes wholely different paradigm of client-side development are still only beginning to be fully explored.

If you're ready to start exploring them too, check out Web Components: from zero to hero, an amazing introduction to these technologies by Pascal Schilp, and you'll be well on the way.

When creating your own custom element with their own shadow roots, you're getting a "DOM subtree that is rendered separately from a document's main DOM tree". A subtree that is separate from the document: a document to itself. Inside of that subtree, you get encapsulation for whatever DOM lives therein from external selectors, a special HTML slot API for composing DOM from the outside of the element, and much more. However, when minding the document.activeElement, it is important to look a little deeper at the specific cost that we pay to get these new capabilities.

document.activeElement points to the element in the document that currently has focus, but what happens when that element isn't actually in the document? If your shadow DOM has focusable elements internal to it, and one of those elements currently has focus, document.activeElement (like all other selectors) will not be able to point directly to it. What it will point to is the first element in the document that includes a shadow DOM. So, taking the following tree into account:

<document>
  <body>
    <h1>Title</h1>
    <custom-element>
      #shadow-root
        <h2>Sub-title</h2>
        <other-custom-element>
          #shadow-root
            <a href="#">This is a link</a> <!-- The link _has_ focus -->
Enter fullscreen mode Exit fullscreen mode

When the <a> element above is focused and document.activeElement is referenced, the value returned will point to the <custom-element> just below the <h1>; not the <a>, not the <other-custom-element> that is its parent, and likely, not what you expected.

A brave new world

Oh no, shadow DOM broke the internet!
- alarmist JS (framework) user

Well, in a word, "no".

With more nuance... shadow DOM has broken the assumption that the specifics of focus in any one component will bleed into all other components, so yes the fragile, fly by night, shoot from the hip internet that was previously the only option available to use is broken if you choose to use shadow DOM and the shadow boundaries that they create. However, if you choose to use shadow DOM and the shadow boundaries that they create, you now have access to a more nuanced, controllable, and refined DOM than ever before. Yes, some things that you may have taken for granted in the past may be a little different than you remember, but you also have access to capabilities that were previously impossible or prohibitively complex.

But... if I can't see what the currently focused element is, what will I do?

While inside a shadow root, document.activeElement will not allow you to see if any other elements in the subtree are currently focused, yes. However, from the root of a subtree, we now have shadowRoot.activeElement available to us when we desire to find the focused element in our current subtree. This means that instead of having to worry about the entire document (both above and below your current component), you can take into consideration only the DOM belonging to the subtree related to the current component.

OK, how do I leverage this?

I feel you start to think, "ok, that sounds like I could find a way to process this as being cool after ruminating on it for a while, but how do I figure out what shadow root I'm in?", and that's a great question! The answer is in the getRootNode() method that has been added to Element as part of the introduction of shadow DOM. With this method, you will be given the root of the DOM tree in which the element you called getRootNode() on lives. Whether what is returned is the actual document or an individual shadowRoot its member property activeElement will allow you to know what element in that tree is currently focused.

Let's revisit our sample document from above to better understand what this means...

<document>
  <body>
    <h1>Title</h1>
    <custom-element>
      #shadow-root
        <h2>Sub-title</h2>
        <other-custom-element>
          #shadow-root
            <a href="#">This is a link</a> <!-- The link _has_ focus -->
Enter fullscreen mode Exit fullscreen mode

When you have a reference to the <a> element therein:

const root = a.getRootNode();
console.log(root);             // otherCustomElement#shadowRoot
const activeElement = root.activeElement;
console.log(activeElement);    // <a href="#"></a>
Enter fullscreen mode Exit fullscreen mode

When you have a reference to the <h2> element therein:

const root = h2.getRootNode();
console.log(root);             // customElement#shadowRoot
const activeElement = root.activeElement;
console.log(activeElement);    // <other-custom-element></other-custom-element>
Enter fullscreen mode Exit fullscreen mode

And, when you have a reference to the <body> element therein:

const root = body.getRootNode();
console.log(root);             // document
const activeElement = root.activeElement;
console.log(activeElement);    // <custom-element></custom-element>
Enter fullscreen mode Exit fullscreen mode

But, a component should have some control of its children, right?

I completely agree! But, in the context of a free and single document "some" control becomes complete and total control.

Not just "some" control...

In the case of shadow DOM encapsulated subtrees, the control that a parent has over its children is only the control that said child offers in the form of its public API. If you don't want to cede any control to a parent element implementing your custom element, you do not have to. Much like the first night you stayed out past curfew, this will surprise most parents accustomed to a level of control they maybe never should have had.

  • Will they get the number to your new cell phone?
  • Will you pick up when they call?
  • Will you still come home for dinner on Sunday nights?

All these questions and more are yours to answer via the attributes, properties, and methods that your elements surface to the public. Take care to respect your parents, but don't think that you have to become a doctor/lawyer/the President just because your mother said you should.

The components are alright

In this way, we might address the following simplification of the DOM we've reviewed through much of this article:

<document>
  <body>
    <h1>Title</h1>
    <other-custom-element>
      #shadow-root
        <a href="#">This is a link</a> <!-- The link _has_ focus -->
Enter fullscreen mode Exit fullscreen mode

When accessing document.activeElement from the outside, again we will be returned other-custom-element in reverence of the constrained control we now have over our once singular document. In this context, we may want to forward a click event into our focused element, however not having direct access to the anchor tag through the shadow boundary, we'd be calling click() on other-custom-element. By default, this type of interaction on the shadow DOM of other-custom-element would be prevented. In the case that we wanted this sort of thing to be possible, we could build the following extension of the click() method into our other-custom-element element to pass the click into its child:

click() {
  this.shadowRoot.querySelector('a').click();
}
Enter fullscreen mode Exit fullscreen mode

But what about the case where there are more than one anchor tags inside of an other-custom-element?

<other-custom-element>
  #shadow-root
    <a href="#">This is a link</a>
    <a href="#">This is also a link</a> <!-- The link _has_ focus -->
Enter fullscreen mode Exit fullscreen mode

In this case, we can take advantage of the activeElement accessor on a shadow root and target the correct anchor tag as follows to make an even more flexible custom element implementation:

click() {
  this.shadowRoot.activeElement.click();
}
Enter fullscreen mode Exit fullscreen mode

From here, there are any number of next steps that you can take to produce your own powerful custom elements that leverage the encapsulation offered by the shadow DOM to structure more nuanced, yet eminently powerful APIs to surface to users of your components. As you find patterns that work well for you, I'd love to hear about them in the comments below. If you're interested in uses of the activeElement property in the wild, I invite you to checkout Spectrum Web Components where we are actively reviewing the use of this and many other practices to power our growing web component implementation of the Spectrum, Abode's design system.

Discussion (3)

Collapse
ianemv profile image
Ian Villanueva

Is there an event inside the web component to detect that is not the current activeElement?

Collapse
westbrook profile image
Westbrook Johnson Author

A focusout event will be dispatched (and bubble, whereas blur events do not) when an element loses focus. This mirrors the focusin event that would have told you that an element has gained focus. You can checkout event.composedPath()[0] to ensure you reference the element from which this even originated. Check out open-wc.org/faq/events.html for more info on handling events in custom elements and across shadow DOM boundaries.

Is that what you're looking for?

At any one time, the following code can compare that:

const el = ... ; // how ever you gain access to the element in question
const root = el.getRootNode();
const { activeElement } = root;
const isElFocused = el === activeElement;
Collapse
ianemv profile image
Ian Villanueva

I think this is it. What I wanted is, to know that my component loses focus so I can toggle or trigger other events.

For now what I did is add a focusable element such as button to check if that is on focus and if focus out, trigger the event I wanted to do (basically just hide a pop-up div)