Can I focus an element in shadow DOM programmatically?

posted on

Back to overview

Yes.

The answer to this question is yes, but there are two different ways of doing it, and how you can do it depends on the type of component you're working with.

Before I can elaborate, you must understand the difference between the two types of shadow DOMs there are. When you attach a shadow tree to a node, you can either create an open or closed shadow DOM.

Open means that JavaScript from the outside has access to the nodes inside the shadow DOM. That’s usually the default.

class TheButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });

    const button = document.createElement("button");
    button.textContent = "Click me";
    button.addEventListener('click', e => alert('yo!'))

    this.shadowRoot.append(button);
  }
}

customElements.define("the-button", TheButton);
A component with an open shadow DOM.

The following query returns 1 because there's one element in the shadow DOM of the component.

const theButton = document.querySelector('the-button');
console.log(theButton.shadowRoot.querySelectorAll('*').length)
// => returns 1
Querying the component and all elements in its shadow root.

Closed denies access to the nodes from the outside.

class TheButton extends HTMLElement {
  constructor() {
    super();
    this._shadow = this.attachShadow({ mode: "closed" });

    const button = document.createElement("button");
    button.textContent = "Click me";
    button.addEventListener('click', e => alert('yo!'))

    this._shadow.append(button);
  }
}

customElements.define("the-button", TheButton);
A component with a closed shadow DOM.

In a closed shadow DOM the same query returns “Cannot read properties of null (reading ‘querySelectorAll’)” because you cannot access the shadow DOM from the outside.

Now that you know that, we can talk about the two solutions.

Accessing the shadowRoot

This solution only works when you're dealing with an open shadow DOM.

const theButton = document.querySelector('the-button')
theButton.shadowRoot.querySelector('button').focus()
Querying the component and focusing the button in its shadow root.

When you click the “Focus” button in light DOM, the “Click me” button in shadow DOM receives focus.

Delegating focus

Another solution that works both with an open and closed shadow DOM is focus delegation.
When you attach the shadow, you can pass another option in addition to the mode, delegatesFocus.

  class TheButton extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open", delegatesFocus: true  });

      const button = document.createElement("button");
      button.textContent = "Click me";
      button.addEventListener('click', e => alert('yo!'))

      this.shadowRoot.append(button);
    }
  }

  customElements.define("the-button", TheButton);
A component with an open shadow DOM that delegates focus.

When it's true, and you call focus() on the host, the first focusable element in the hosts shadow DOM receives focus.

const btn = document.querySelector('button')
btn.addEventListener('click', e => document.querySelector('the-button').focus())
Focusing the component, focuses the button.

Back to overview