Event Retargeting
This lesson explains event retargeting, why Shadow DOM changes event.target for outside listeners, and how to build reliable event handling across encapsulation boundaries.
What Is Event Retargeting?
Event retargeting is browser behavior that changes what outside
code sees as event.target when an event crosses a
Shadow DOM boundary. A click on an internal shadow button may
appear to outside listeners as a click on the host element
<app-card> instead.
Retargeting protects component internals. Parent pages should not
depend on private shadow DOM structure, but they still need
reliable ways to react to user interaction.
| Concept | Description |
event.target | The element that triggered the event |
event.currentTarget | The element with the listener attached |
| Retargeting | Target rewritten at shadow boundary |
composedPath() | Full path including internal nodes |
composed: true | Lets custom events escape Shadow DOM |
| Goal | Encapsulation without breaking communication |
Basic Retargeting Example
class AppCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<button type="button">Open</button>
`;
}
}
customElements.define("app-card", AppCard);
document.addEventListener("click", (event) => {
console.log(event.target.tagName); // APP-CARD
});
const card = document.querySelector("app-card");
card.shadowRoot.querySelector("button")
.addEventListener("click", (event) => {
console.log(event.target.tagName); // BUTTON
});
The same click shows different targets depending on where the
listener is attached. Outside the shadow tree, the host is the
visible target.
How Retargeting Works
| Listener Location | Typical event.target |
| Inside shadow root | Actual internal element (button, input, div) |
| On host element | Host custom element |
| On document or body | Host custom element after retargeting |
| On slotted light DOM node | Slotted element, with slot-specific rules |
Retargeting happens as the event crosses the shadow boundary
during bubbling or capture. It is automatic browser behavior,
not something component authors manually configure.
target vs currentTarget
appCard.addEventListener("click", (event) => {
console.log(event.target); // often the host after retargeting
console.log(event.currentTarget); // always appCard
});
currentTarget is the element you attached the
listener to. target is the retargeted origin from
the perspective of that listener. Confusing the two is a common
source of bugs in delegated event handlers.
Using composedPath()
When you need more detail than retargeted
event.target, use event.composedPath().
It returns the full propagation path, including internal shadow
nodes when the event is composed.
document.addEventListener("click", (event) => {
const path = event.composedPath();
console.log(path[0]); // deepest node in the path
console.log(path.includes(appCard)); // true
});
composedPath() is especially useful for debugging,
analytics, and advanced delegation patterns that must understand
which component was interacted with.
Event Delegation Pitfall
document.body.addEventListener("click", (event) => {
if (event.target.matches("button.save")) {
// This may fail for buttons inside Shadow DOM
save();
}
});
Global delegation that inspects event.target breaks
when internal buttons are hidden behind retargeting. The target
looks like the host element, not the internal
button.save.
Reliable Delegation Patterns
- Listen on the host custom element, not only on
document. - Emit composed custom events from inside the component.
- Use
composedPath() to inspect internal nodes when needed. - Expose semantic events such as
save or select. - Avoid coupling page code to private shadow selectors.
this.dispatchEvent(
new CustomEvent("save", {
detail: { id: this.itemId },
bubbles: true,
composed: true
})
);
Public custom events are the cleanest contract between a component
and its parent application.
Native Events That Retarget
| Event Type | Retargeting Note |
click | Common retargeting case for internal buttons and links |
focus / blur | Focus events retarget at shadow boundaries |
input / change | Form-related events follow shadow retargeting rules |
keydown / keyup | Keyboard events may retarget for outside listeners |
| Composed custom events | Designed to cross boundaries intentionally |
Retargeting and Slots
<app-dialog>
<button slot="actions">Confirm</button>
</app-dialog>
Slotted nodes live in the light DOM but render inside the
component layout. Event retargeting for slotted content can
surprise developers because the visual location and event target
do not always match intuition.
For slotted interactive content, prefer listening inside the
component or emitting explicit custom events rather than assuming
global event.target inspection will work.
Retargeting in Framework Apps
React, Vue, and Angular often attach listeners on components or
use synthetic event systems. Web Components still follow native
retargeting rules at the DOM level.
// React: listen for composed custom events with a ref
useEffect(() => {
const node = ref.current;
const onSave = (event) => console.log(event.detail);
node.addEventListener("save", onSave);
return () => node.removeEventListener("save", onSave);
}, []);
Framework wrappers should not assume they can reach into shadow
DOM nodes. They should consume the component's public event
API instead.
Open vs Closed Shadow DOM
| Shadow Mode | Outside Access to Internals | Retargeting |
open | element.shadowRoot is available | Still retargets for normal outside listeners |
closed | No public shadowRoot reference | Still retargets; internals are harder to inspect |
Retargeting happens in both modes. Closed shadow roots simply make
it harder to bypass the public API by querying internals directly.
Debugging Retargeted Events
element.addEventListener("click", (event) => {
console.log("target:", event.target);
console.log("currentTarget:", event.currentTarget);
console.log("path:", event.composedPath());
console.log("composed:", event.composed);
});
Log all four values when an event handler behaves unexpectedly.
Retargeting issues often show up as the correct action happening
on the wrong perceived target.
Full Component Event Pattern
class ProductCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<article>
<h2><slot name="title"></slot></h2>
<button type="button">Add to cart</button>
</article>
`;
}
connectedCallback() {
this.shadowRoot
.querySelector("button")
.addEventListener("click", () => this.emitAddToCart());
}
emitAddToCart() {
this.dispatchEvent(
new CustomEvent("add-to-cart", {
detail: { sku: this.getAttribute("sku") },
bubbles: true,
composed: true
})
);
}
}
document.querySelector("product-card")
.addEventListener("add-to-cart", (event) => {
console.log(event.detail.sku);
});
Internal clicks stay encapsulated. The composed custom event gives
parent code a stable, semantic hook without relying on retargeted
native click targets.
When Retargeting Matters
- You use Shadow DOM in reusable components.
- Parent pages use document-level event delegation.
- You need to expose interaction without leaking internals.
- Analytics or tracking inspect
event.target. - Framework apps listen for native DOM events on custom tags.
- You debug focus, click, or keyboard behavior across boundaries.
Event Retargeting Best Practices
- Design a public custom event API for component outputs.
- Use
composed: true for events parents must hear. - Do not depend on global
event.target matching internal selectors. - Use
composedPath() only when semantic events are not enough. - Document which events a component emits and what
detail contains. - Handle focus and keyboard events with retargeting in mind.
- Test listeners on the host, document, and inside the shadow root.
Common Retargeting Mistakes
- Assuming
event.target is the internal button. - Using document delegation with shadow-only CSS selectors.
- Forgetting
composed: true on custom events. - Exposing internal DOM nodes through
detail. - Mixing
target and currentTarget incorrectly. - Expecting closed shadow DOM to disable retargeting.
- Building framework integrations that query private shadow nodes.
Key Takeaways
- Shadow DOM retargets
event.target for outside listeners. - Retargeting protects internal component structure.
composedPath() reveals the full event path. - Global event delegation is fragile with shadow internals.
- Composed custom events are the best public communication API.
- Understanding retargeting prevents subtle cross-boundary bugs.
Pro Tip
If parent code needs to react to an internal action, do not make it
inspect event.target. Emit a composed custom event
with a clear name and detail payload instead.