Custom Events

The browser provides many built-in events, but you can also create your own. Custom events are useful when one part of your application needs to tell another part that something happened, without the two parts being tightly coupled.

Creating Custom Events

Use the CustomEvent constructor to create an event:

const event = new CustomEvent("userLoggedIn");

Dispatch the event on an element:

document.dispatchEvent(event);

Listen for the event like any other:

document.addEventListener("userLoggedIn", () => {
  console.log("User has logged in!");
});

Passing Data with Events

The detail property allows you to attach data to your event:

const event = new CustomEvent("userLoggedIn", {
  detail: {
    userId: 123,
    username: "alice",
    timestamp: Date.now(),
  },
});

document.dispatchEvent(event);

Access the data in the handler:

document.addEventListener("userLoggedIn", (event) => {
  console.log("User ID:", event.detail.userId);
  console.log("Username:", event.detail.username);
});

Practical Example: Shopping Cart

// Cart module dispatches events
const cart = {
  items: [],

  addItem(product) {
    this.items.push(product);

    document.dispatchEvent(
      new CustomEvent("cart:itemAdded", {
        detail: { product, cartTotal: this.items.length },
      }),
    );
  },

  removeItem(productId) {
    this.items = this.items.filter((p) => p.id !== productId);

    document.dispatchEvent(
      new CustomEvent("cart:itemRemoved", {
        detail: { productId, cartTotal: this.items.length },
      }),
    );
  },
};

// UI module listens and updates
document.addEventListener("cart:itemAdded", (event) => {
  updateCartBadge(event.detail.cartTotal);
  showNotification(`Added ${event.detail.product.name} to cart`);
});

document.addEventListener("cart:itemRemoved", (event) => {
  updateCartBadge(event.detail.cartTotal);
});

CustomEvent Options

CustomEvent accepts additional options:

const event = new CustomEvent("myEvent", {
  detail: { data: "value" },
  bubbles: true,  // Event bubbles up the DOM
  cancelable: true,  // preventDefault() can be called
  composed: true,  // Event can cross shadow DOM boundaries
});

Bubbling Custom Events

With bubbles: true, the event propagates like native events:

// Dispatch from a child element
const child = document.querySelector("#child");
const event = new CustomEvent("childAction", {
  bubbles: true,
  detail: { action: "clicked" },
});

child.dispatchEvent(event);

// Listen on a parent
document.querySelector("#parent").addEventListener("childAction", (event) => {
  console.log("Child did:", event.detail.action);
});

Cancelable Events

With cancelable: true, listeners can prevent the default action:

const event = new CustomEvent("beforeSave", {
  cancelable: true,
  detail: { data: formData },
});

document.dispatchEvent(event);

if (event.defaultPrevented) {
  console.log("Save was cancelled");
} else {
  performSave();
}

// A listener can cancel:
document.addEventListener("beforeSave", (event) => {
  if (!isValid(event.detail.data)) {
    event.preventDefault();
  }
});

Naming Conventions

Use a namespace prefix to avoid conflicts with native events:

// Good - namespaced
"app:userLoggedIn";
"cart:itemAdded";
"modal:opened";

// Avoid - could conflict with future native events
"login";
"add";
"open";

Custom Events vs Callbacks

Custom events are useful when:

  • Multiple unrelated parts of the app need to respond
  • You want loose coupling between modules
  • The responding code might not exist yet

Use callbacks when:

  • There is a single, known responder
  • You need a return value
  • The relationship is clear and direct