Event Retargeting in Shadow DOM — Complete Guide
In this tutorial, you will learn about Event Retargeting in Shadow Dom. We cover key concepts, practical examples, and best practices to help you master this topic.
Event retargeting adjusts event target properties when events cross shadow boundaries, making custom elements appear as a single logical unit to outside code.
What You'll Learn
- What event retargeting is and why the browser does it
- How event.target changes across shadow boundaries
- The difference between composed and non-composed events
- How to handle events from inside Shadow DOM
Why It Matters
Without retargeting, external code would see internal DOM nodes inside a shadow tree, breaking Encapsulation. Retargeting ensures a custom element appears as one unit to outside listeners.
flowchart LR A[User clicks inside Shadow DOM] --> B[Event fires on internal element] B --> C[Browser retargets event] C --> D[event.target becomes host element] D --> E[External listener sees host, not internal node] E --> F[Encapsulation preserved]
How Event Retargeting Works
When an event fires on an element inside a shadow tree and crosses the shadow boundary to reach a listener on the host or above, the browser adjusts event.target to point to the shadow host element. This prevents outside code from knowing the internal structure.
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<button id="innerBtn">Click me</button>';
document.body.appendChild(host);
host.addEventListener('click', (event) => {
// Without retargeting: event.target would be the button
// With retargeting: event.target is the host
console.log('Target:', event.target);
console.log('Is host?', event.target === host);
});
// Clicking the button logs:
// Target: <div> (the host)
// Is host? true
Composed vs Non-Composed Events
Events in Shadow DOM have a composed property. Composed events cross shadow boundaries. Non-composed events stay inside the shadow tree.
// Most UI events are composed: click, keydown, focus, mouse events
// Some events are not composed: slotchange, DOMNodeInserted, custom events
class EventDemo extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<button id="btn">Test</button>';
this.shadowRoot.getElementById('btn').addEventListener('click', (e) => {
console.log('Composed:', e.composed); // true for click
});
}
}
customElements.define('event-demo', EventDemo);
// Outside listener sees retargeted target for composed events
Custom Events and Composed Flag
When you dispatch custom events from inside Shadow DOM, you must set composed: true for them to cross the boundary.
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<button id="notify">Notify</button>';
this.shadowRoot.getElementById('notify').addEventListener('click', () => {
// Without composed: true, this event stays inside the shadow tree
const event = new CustomEvent('my-action', {
bubbles: true,
composed: true, // Required to cross shadow boundary
detail: { message: 'Button was clicked' }
});
this.dispatchEvent(event);
});
}
}
customElements.define('my-component', MyComponent);
// External listener can now hear 'my-action' events
document.querySelector('my-component').addEventListener('my-action', (e) => {
console.log('Received:', e.detail.message);
// Output: Received: Button was clicked
});
Event Retargeting with Slots
When events come from slotted content (Light DOM projected into Shadow DOM), retargeting behaves differently. Slotted content lives in Light DOM, so its events do not get retargeted when heard from the host.
class SlotHost extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = '<slot></slot>';
this.addEventListener('click', (e) => {
// For slotted content: e.target is the actual slotted element
// NOT the host. Slotted content is not retargeted.
console.log('Target from slot click:', e.target);
});
}
}
customElements.define('slot-host', SlotHost);
// Usage: <slot-host><button>I am slotted</button></slot-host>
// Clicking the button shows the button as target, not the host
Common Mistakes
- Forgetting to set
composed: trueon custom events dispatched from Shadow DOM, causing them to be trapped inside the shadow tree. - Expecting slotted content events to be retargeted like shadow tree events.
- Using
event.targetinside a shadow tree listener and assuming it refers to the host. - Relying on
event.composedPath()without understanding it includes internal nodes through all shadow boundaries. - Dispatching events from inside a constructor before the element is connected to the DOM.
Practice Questions
- What does event retargeting do? It adjusts event.target to the shadow host when events cross shadow boundaries.
- Which events are composed by default? Most UI events like click, keydown, focus, and mouse events.
- How do you make a custom event cross shadow boundaries? Set
composed: truein the event options. - Why are slotted content events not retargeted? Because slotted content lives in Light DOM, not inside the shadow tree.
Challenge
Create a custom element with nested shadow trees. Dispatch a custom event from the innermost shadow tree with composed: false first, then with composed: true. Observe which listeners receive the event at each level.
FAQ
Mini Project
Build a button group component where each inner button dispatches a composed custom event. The host listens and logs which button was clicked. Then add an outside listener that receives the custom events and updates a status display.
What's Next
Lesson 12: Focus Management in Shadow DOM
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro