Skip to content

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.