An event bus is a design pattern (and while we’ll be talking about JavaScript here, it’s a design pattern in any language) that can be used to simplify communications between different components. It can also be thought of as publish/subscribe or pubsub.
The idea is that components can listen to the event bus to know when to do the things they do. For example, a “tab panel” component might listen for events telling it to change the active tab. Sure, that might happen from a click on one of the tabs, and thus handled entirely within that component. But with an event bus, some other elements could tell the tab to change. Imagine a form submission which causes an error that the user needs to be alerted to within a specific tab, so the form sends a message to the event bus telling the tabs component to change the active tab to the one with the error. That’s what it looks like aboard an event bus.
Pseudo-code for that situation would be like…
// 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);
Do you need a JavaScript library to this? (Trick question: you never need a JavaScript library). Well, there are lots of options out there:
- PubSubJS
- EventEmitter3
- Postal.js
- jQuery even supported custom events, which is highly related to this pattern.
Also, check out Mitt which is a library that’s only 200 bytes gzipped. There is something about this simple pattern that inspires people to tackle it themselves in the most succincet way possible.
Let’s do that ourselves! We’ll use no third-party library at all and leverage an event listening system that is already built into JavaScript with the addEventListener
we all know and love.
First, a little context
The addEventListener
API in JavaScript is a member function of the EventTarget
class. The reason we can bind a click
event to a button is because the prototype interface of <button>
(HTMLButtonElement
) inherits from EventTarget
indirectly.

Different from most other DOM interfaces, EventTarget
can be created directly using the new
keyword. It is supported in all modern browsers, but only fairly recently. As we can see in the screenshot above, Node
inherits EventTarget
, thus all DOM nodes have method addEventListener
.
Here’s the trick
I’m suggesting an extremely lightweight Node
type to act as our event-listening bus: an HTML comment (<!--
comment
-->
).
To a browser rendering engine, HTML comments are just notes in the code that have no functionality other than descriptive text for developers. But since comments are still written in HTML, they end up in the DOM as real nodes and have their own prototype interface—Comment
—which inherits Node
.
The Comment
class can be created from new
directly like EventTarget
can:
const myEventBus = new Comment('my-event-bus');
We could also use the ancient, but widely-supported document.createComment
API. It requires a data
parameter, which is the content of the comment. It can even be an empty string:
const myEventBus = document.createComment('my-event-bus');
Now we can emit events using dispatchEvent
, which accepts an Event
Object. To pass user-defined event data, use CustomEvent
, where the detail
field can be used to contain any data.
myEventBus.dispatchEvent(
new CustomEvent('event-name', {
detail: 'event-data'
})
);
Internet Explorer 9-11 supports CustomEvent
, but none of the versions support new CustomEvent
. It’s complex to simulate it using document.createEvent
, so if IE support is important to you, there’s a way to polyfill it.
Now we can bind event listeners:
myEventBus.addEventListener('event-name', ({ detail }) => {
console.log(detail); // => event-data
});
If an event intends to be triggered only once, we may use { once: true }
for one-time binding. Other options won’t fit here. To remove event listeners, we can use the native removeEventListener
.
Debugging
The number of events bound to single event bus can be huge. There also can be memory leaks if you forget to remove them. What if we want to know how many events are bound to myEventBus
?
myEventBus
is a DOM node, so it can be inspected by DevTools in the browser. From there, we can find the events in the Elements → Event Listeners tab. Be sure to uncheck “Ancestors” to hide events bound on document
and window
.

An example
One drawback is that the syntax of EventTarget
is slightly verbose. We can write a simple wrapper for it. Here is a demo in TypeScript below:
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
The following demo provides the compiled JavaScript.
And there we have it! We just created a dependency-free event-listening bus where one component can inform another component of changes to trigger an action. It doesn’t take a full library to do this sort of stuff, and the possibilities it opens up are pretty endless.
Cool :)
Please consider two other options before choosing this sort of approach:
Just use the window object, which is already there. Your wrapper doesn’t add any new functionality, and if you’re not already familiar with the standard browser API, now is a good time to learn. Using the window object enables listeners to register before the script that emits events has even been loaded – whereas your wrapper becomes a new dependency that subscribers have to load or locate somehow. Use your vendor name as an event-name prefix to make sure you don’t collide with third-party scripts that may be using the same bus.
If you’re targeting Node, where popular packages for this are already available, and you still insist on rolling your own, using the standard Map and Set classes to store listeners, requires just a few more lines of code. This will work with anything that runs JavaScript outside the browser.
If you’re doing this for “shorter method names”, you’re doing it for the wrong reasons – if you’re developing for the browser, you will almost certainly have to use the native event features, and nothing is won by creating an identical feature with inconsistent names.
If you’re targeting just the browser, the feature you’re looking for is already there, and fully standardized. When we get opinionated about these things, we’re just creating useless complexity and extra learning curve for the next person who has to maintain the product.
Always go for simplicity if you have the choice.
The DOM you are using as an event bus should be clean, I don’t think using vendor prefixes is a good idea, since it’s verbose and still has posibilities conflicting with other events
IMO the event bus DOM should not emit native events itself, should not have any affects in the DOM tree and should not be reused by other 3rd party libs.
Using window is the worst choise IMO because every event emitted in the page will be bobbled to window and there are huge amount of 3rd party libs binding events on the window, they can be problems.
You can think of event bus as hooks in WordPress. This one uses native JavaScript, but doesn’t use the Event API so it supports more browsers. Suitable to be extended in a tiny application:
https://github.com/taufik-nurrohman/hook
I didn’t understood why create a comment node instead of using the component’s “container”, as it already exists?
Because they can emit native events which may interfere with other subscribers
Which is the difference between doing “new Comment” (or any other DOM Element) and creating directly a new EventTarget Instance through
new EventTarget()
except of having backward compatibility with IE?I haven’t tried yet, but I will do soon.
I think that only the idea to use a comment for an event bus is crazy and fun. I wouldn’ thought of it, so thanks for this one. I’ve learned something new today.
Because a comment can be inspected by browser devtools which is useful for debugging.
See debug section
I’ve been using the same technique with an empty
div
. Thank you for sharing this in details. Now I learned I can go even lighter with a bare EventTarget instance. (I don’t need to support IE11.)I like your series.
But why is the title ‘…Native Event Bus in JavaScript’, but do you end with Typescript?
What is the javascript class example?
Well, typescript is typically javascript with types. Just remove these type assertion you get javascript.
it is not necessary to use a DOM comment to take advantage of EventTarget. Here is my proposal for the EventBus :
Thanks so much for sharing the code for extending off EventTarget. I think the author said to use DOM comment because of lack of browser support for EventTarget.
This is EXACTLY what I have been searching for. I am a software engineer that focuses on data and am totally new to TypeScript development. I have a question. Does anyone know of any reason why this WOULD NOT WORK to wire up communication between 3rd party libraries such as Synchfusion or Telerik? I am having a very hard time enabling cross-component method invocation.
the off method does not seems to be functional
I really like the idea, of using a comment as an event bus, because you can be sure it won’t be interfering with any layout. But how do I select this comment js wise.