Mind the `document.activeElement`!
The element that currently hasfocus in your HTML at any point in time can be accessed asdocument.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.
We are gathered here, today, to go a little bit deeper on whatdocument
means and whenthedocument
isn't the “document”0 you're looking for and what to do in that case.
Out of the shadows a newdocument
rises...
Do you find yourself using code like the following to attach a shadow root to elements in your application?
el.attachShadow({mode:'open'});
Do you find yourself attaching that shadow root to custom elements that you've defined?
classCustomElementextendsHTMLElement{}customElement.define('custom-element',CustomElement);
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 outWeb Components: from zero to hero, an amazing introduction to these technologies byPascal 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 thedocument
: adocument
to itself. Inside of that subtree, you get encapsulation for whatever DOM lives therein from external selectors, a special HTMLslot
API for composing DOM from the outside of the element, and much more. However, when minding thedocument.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 thedocument
that currently hasfocus, but what happens when that element isn't actually in thedocument
? If your shadow DOM has focusable elements internal to it, and one of those elements currently hasfocus,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 thedocument
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<ahref="#">This is a link</a><!-- The link _has_ focus -->
When the<a>
element above is focused anddocument.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 offocus 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 isbroken 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, whatwill 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 haveshadowRoot.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 thegetRootNode()
method that has been added toElement
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 calledgetRootNode()
on lives. Whether what is returned is the actualdocument
or an individualshadowRoot
its member propertyactiveElement
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<ahref="#">This is a link</a><!-- The link _has_ focus -->
When you have a reference to the<a>
element therein:
constroot=a.getRootNode();console.log(root);// otherCustomElement#shadowRootconstactiveElement=root.activeElement;console.log(activeElement);// <a href="#"></a>
When you have a reference to the<h2>
element therein:
constroot=h2.getRootNode();console.log(root);// customElement#shadowRootconstactiveElement=root.activeElement;console.log(activeElement);// <other-custom-element></other-custom-element>
And, when you have a reference to the<body>
element therein:
constroot=body.getRootNode();console.log(root);// documentconstactiveElement=root.activeElement;console.log(activeElement);// <custom-element></custom-element>
But, a componentshould have some control of its children, right?
I completely agree! But, in the context of a free and singledocument
"some" control becomescomplete and total 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<ahref="#">This is a link</a><!-- The link _has_ focus -->
When accessingdocument.activeElement
from the outside, again we will be returnedother-custom-element
in reverence of the constrained control we now have over our once singulardocument
. In this context, we may want to forward aclick
event into our focused element, however not having direct access to the anchor tag through the shadow boundary, we'd be callingclick()
onother-custom-element
. By default, this type of interaction on the shadow DOM ofother-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 theclick()
method into ourother-custom-element
element to pass theclick
into its child:
click() { this.shadowRoot.querySelector('a').click();}
But what about the case where there are more than one anchor tags inside of another-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 -->
In this case, we can take advantage of theactiveElement
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();}
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 theactiveElement
property in the wild, I invite you to checkoutSpectrum Web Components where we are actively reviewing the use of this and many other practices to power our growing web component implementation of theSpectrum, Abode's design system.
Top comments(3)

Afocusout
event will be dispatched (and bubble, whereasblur
events do not) when an element loses focus. This mirrors thefocusin
event that would have told you that an element has gained focus. You can checkoutevent.composedPath()[0]
to ensure you reference the element from which this even originated. Check outopen-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 questionconst root = el.getRootNode();const { activeElement } = root;const isElFocused = el === activeElement;
For further actions, you may consider blocking this person and/orreporting abuse