A Native Event Bus: No Library Required
An event bus — also known as publish/subscribe or pubsub — is a design pattern for simplifying communication between components. Instead of components calling each other directly, they listen for events and react accordingly. For instance, a tab panel component might listen for events telling it to change the active tab. That change could come from a click on one of the tabs, but with an event bus, any other element can trigger it — say, a form submission that needs to direct the user to a specific tab containing an error.
Pseudo-code for that scenario:
// Tab Component
Tabs.changeTab = id => {
// DOM work to change the active tab.
}
MyEventBus.subscribe("change-tab", Tabs.changeTab(id));
// Some other component...
// something happens, then:
MyEventBus.publish("change-tab", 2);
You don't need a library for this — though options like PubSubJS, EventEmitter3, Postal.js, or the tiny Mitt (200 bytes gzipped) exist. The pattern is simple enough that it's often tempting to hand-roll it. You can do that by leveraging the addEventListener API already built into JavaScript.
EventTarget Can Be Instantiated Directly
The addEventListener method belongs to the EventTarget class. A button can bind click events because its prototype interface (HTMLButtonElement) ultimately inherits from EventTarget.

Unlike most DOM interfaces, EventTarget can be instantiated directly with new. It's supported in all modern browsers, though only fairly recently. As seen above, Node inherits EventTarget, so all DOM nodes have addEventListener.
Using a Comment as Your Event Bus
A convenient, ultra-lightweight node to act as the event bus: an HTML comment. Browsers treat comments as inert notes, but they still become real DOM nodes with their own prototype interface — Comment — which inherits Node.
The Comment class can also be created with new:
const myEventBus = new Comment('my-event-bus');
Alternatively, the widely-supported document.createComment API works too. It requires a data parameter (the comment's content), which can be an empty string:
const myEventBus = document.createComment('my-event-bus');
Emitting and Listening
To emit events, use dispatchEvent, which accepts an Event object. For user-defined data, use CustomEvent, where the detail field can carry any payload:
myEventBus.dispatchEvent(
new CustomEvent('event-name', {
detail: 'event-data'
})
);
Internet Explorer 9–11 supports CustomEvent but not new CustomEvent. If IE support matters, a polyfill is available.
Listening is no different from binding to any DOM node:
myEventBus.addEventListener('event-name', ({ detail }) => {
console.log(detail); // => event-data
});
For one-time triggers, pass { once: true } in the listener options. To unbind, use removeEventListener.
Debugging and a Practical Wrapper
A single event bus can accumulate many listeners, and forgetting to remove them can leak memory. To inspect what's bound, open DevTools and examine myEventBus — it's a DOM node — under Elements → Event Listeners. Uncheck “Ancestors” to filter out events on document and window:

The native EventTarget syntax can feel verbose, though. A thin wrapper helps. Here's a TypeScript version:
class EventBus<DetailType = any> {
private eventTarget: EventTarget;
constructor(description = '') { this.eventTarget = document.appendChild(document.createComment(description)); }
on(type: string, listener: (event: CustomEvent<DetailType>) => void) { this.eventTarget.addEventListener(type, listener); }
once(type: string, listener: (event: CustomEvent<DetailType>) => void) { this.eventTarget.addEventListener(type, listener, { once: true }); }
off(type: string, listener: (event: CustomEvent<DetailType>) => void) { this.eventTarget.removeEventListener(type, listener); }
emit(type: string, detail?: DetailType) { return this.eventTarget.dispatchEvent(new CustomEvent(type, { detail })); }
}
// Usage
const myEventBus = new EventBus<string>('my-event-bus');
myEventBus.on('event-name', ({ detail }) => {
console.log(detail);
});
myEventBus.once('event-name', ({ detail }) => {
console.log(detail);
});
myEventBus.emit('event-name', 'Hello'); // => Hello Hello
myEventBus.emit('event-name', 'World'); // => World
A demo of the compiled JavaScript follows.
That's it — a dependency-free event bus built on APIs you already know, with any component able to broadcast while others listen and react.



