Why You Should Prefix Custom Events

Similar to how every custom element must contain a dash, I like to pretend that every custom event must also contain a dash. This removes all ambiguity between native events and custom events.

Therefore, I prefix all custom events in Shoelace with sl-. It may seem contradictory that I've chosen to follow the platform's convention for emitting events but not for naming them. There's a good reason, though.

To preserve encapsulation, events that emit from within a shadow root are retargeted so they appear to come from the host element. If a custom element has three buttons in its shadow root, a click on any of those buttons will appear to come from the host element, as demonstrated below.

<!-- A custom element with three buttons in its shadow root -->
<three-buttons></three-buttons>

<script>
  // Define the custom element
  class ThreeButtons extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });

      // There are better ways, but for brevity…
      this.shadowRoot.innerHTML = `
        <button type="button">First</button>
        <button type="button">Second</button>
        <button type="button">Third</button>
      `;
    }
  }

  // Register it
  customElements.define('three-buttons', ThreeButtons);

  // Notice how the target is the same, regardless of which button you click?
  document.querySelector('three-buttons').addEventListener('click', event => {
    console.log(event.target);
  });
</script>

Try it on CodePen

Retargeting is necessary and sometimes even convenient but, because of it, you might observe a seemingly endless number of events getting emitted from the host element as the user interacts with its internals.

So how can you differentiate a click event emitted by an internal element from a custom click event emitted by the host?

You can't rely on the event phase because it will show AT_TARGET. You probably shouldn't use Event.composedPath() because that breaks encapsulation, relies on private structures that can change, and doesn't work for closed shadow roots.

(Flips table over and walks out)

To do this, devs would need to listen to and prevent every event that might be retargeted from any applicable element inside the shadow root, and then re-emit an even of the same name with some kind of payload in event.detail to help differentiate. This would mean a lot of unnecessary code and clutter, and even the most astute custom element authors will fail to get it right from time to time.

Instead, you can simply prefix custom events to sidestep this problem.*

Another benefit of prefixing is that, in the context of a web component library, it's immediately clear which events belong to a particular library. It's fairly obvious that sl-change is a Shoelace event, for example.

*It could be argued that one shouldn't prefix custom events and should instead prevent retargeted events from conflicting with custom events, but I avoid this pattern because it breaks the expectation that consumers may have for handling retargeted events.