new CustomEvent(type, { detail }) carries any data in event.detail, and since 2020 all major browsers let your classes extend EventTarget, so plain objects can emit events without a library.
class Cart extends EventTarget {
#items = [];
add(item) {
this.#items.push(item);
this.dispatchEvent(new CustomEvent("cart:change", { detail: this.#items.length }));
}
}
const cart = new Cart();
cart.addEventListener("cart:change", (e) => console.log(`${e.detail} item(s)`));
cart.add("pen");
cart.add("ink");Output
1 item(s) 2 item(s)
Prefix custom names (cart:change) to avoid clashes with future built-in events. From inside a shadow root, add bubbles: true, composed: true.