Fixing Focus Management in Nested Web Components

The Problem

While building a drawer component system, I encountered a peculiar focus management bug. When opening a drawer, the initial focus was supposed to land on the Close button. Instead, the ag-drawer element itself was receiving focus…


This content originally appeared on DEV Community and was authored by Rob Levin

The Problem

While building a drawer component system, I encountered a peculiar focus management bug. When opening a drawer, the initial focus was supposed to land on the Close button. Instead, the ag-drawer element itself was receiving focus, and our focus detection utility was returning an empty arrayeven though the Close button was clearly visible and focusable.

The Architecture

Our drawer component has an interesting architecture:

<ag-drawer>
  <p>Drawer content</p>
  <ag-button>Close</ag-button>
</ag-drawer>

Internally, ag-drawer renders an ag-dialog in its Shadow DOM:

// ag-drawer's shadow DOM
render() {
  return html`
    <ag-dialog
      .open=${this.open}
      .heading=${this.heading}
      // ... other props
    >
      <slot></slot>
    </ag-dialog>
  `;
}

So the component hierarchy looks like this:

ag-drawer (light DOM: <ag-button>Close</ag-button>)
   shadow root
       ag-dialog
           shadow root
               <slot> (projects ag-drawer's light DOM)

The First Bug: Custom Element Visibility Detection

Our getFocusableElements utility was filtering out the ag-button because of this check:

// Exclude elements that are not visible
if (el.offsetParent === null && window.getComputedStyle(el).position !== 'fixed') {
  return false;
}

Custom elements can have offsetParent === null even when visible, especially:

  • During render cycles
  • After parent transitions
  • In Shadow DOM contexts

Fix: Skip the offsetParent check for custom elements:

const isCustomElement = el.tagName.includes('-');
if (!isCustomElement && el.offsetParent === null && window.getComputedStyle(el).position !== 'fixed') {
  return false;
}

The Second Bug: Wrong Light DOM Search Scope

Even after fixing the custom element detection, the Close button still wasn't found. The issue? We were searching the wrong light DOM container.

ag-dialog was calling:

getFocusableElements(this.shadowRoot, this) // 'this' is ag-dialog

But the slotted content (Close button) lives in ag-drawer's light DOM, not ag-dialog's. ag-dialog's light DOM is emptyit's just a slot that projects content from its parent host.

Fix: Find the parent host element when inside a shadow root:

private _setInitialFocus() {
  // For drawers, the slotted content is in the parent ag-drawer's light DOM
  const lightDomContainer = (this.getRootNode() as ShadowRoot).host as HTMLElement || this;
  const focusableElements = getFocusableElements(this.shadowRoot, lightDomContainer);

  if (focusableElements.length > 0) {
    focusableElements[0].focus();
  }
}

Now ag-dialog correctly searches ag-drawer's light DOM for focusable elements.

Key Takeaways

  1. Custom elements need special visibility handling - Standard DOM visibility checks like offsetParent === null can incorrectly filter out custom elements.

  2. Understand your Shadow DOM hierarchy - When searching for slotted content, you need to search the correct host's light DOM, not the component doing the rendering.

  3. getRootNode().host is your friend - It helps you traverse up the Shadow DOM tree to find parent custom elements.

The Result

Focus management now works correctly:

  • Close button receives initial focus when drawer opens
  • Focus trap properly cycles through all focusable elements
  • Custom elements are correctly identified as focusable

This was a great reminder that Shadow DOM creates encapsulation boundaries that require careful thought when implementing cross-boundary features like focus management.


This content originally appeared on DEV Community and was authored by Rob Levin


Print Share Comment Cite Upload Translate Updates
APA

Rob Levin | Sciencx (2025-10-06T00:33:31+00:00) Fixing Focus Management in Nested Web Components. Retrieved from https://www.scien.cx/2025/10/06/fixing-focus-management-in-nested-web-components/

MLA
" » Fixing Focus Management in Nested Web Components." Rob Levin | Sciencx - Monday October 6, 2025, https://www.scien.cx/2025/10/06/fixing-focus-management-in-nested-web-components/
HARVARD
Rob Levin | Sciencx Monday October 6, 2025 » Fixing Focus Management in Nested Web Components., viewed ,<https://www.scien.cx/2025/10/06/fixing-focus-management-in-nested-web-components/>
VANCOUVER
Rob Levin | Sciencx - » Fixing Focus Management in Nested Web Components. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/10/06/fixing-focus-management-in-nested-web-components/
CHICAGO
" » Fixing Focus Management in Nested Web Components." Rob Levin | Sciencx - Accessed . https://www.scien.cx/2025/10/06/fixing-focus-management-in-nested-web-components/
IEEE
" » Fixing Focus Management in Nested Web Components." Rob Levin | Sciencx [Online]. Available: https://www.scien.cx/2025/10/06/fixing-focus-management-in-nested-web-components/. [Accessed: ]
rf:citation
» Fixing Focus Management in Nested Web Components | Rob Levin | Sciencx | https://www.scien.cx/2025/10/06/fixing-focus-management-in-nested-web-components/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.