Keep Multiple Browser Tabs in Sync with JavaScript
The interesting part is that each tab normally has its own JavaScript state.
If something changes in one tab, the others don't automatically know about it.
Imagine, for example, a simple note editor. You open the application in two tabs and start typing in the first one.
Without additional logic, the second tab stays unchanged.
Fortunately, JavaScript provides a small browser API designed specifically for communication between different instances of the same application:
BroadcastChannel.
Creating a channel
Creating a channel requires only a name:
const channel = new BroadcastChannel("notes");
Any other tab from the same origin can create a channel using the same name:
const channel = new BroadcastChannel("notes");
Those tabs can now exchange messages.
There is no server involved, no WebSocket connection and no polling loop running in the background.
Sending a message to another tab
Let's use a very small example with a text area:
<textarea id="note"></textarea>
Whenever its content changes, we can send the new value through our channel:
const channel = new BroadcastChannel("notes");
const note = document.querySelector("#note");
note.addEventListener("input", () => {
channel.postMessage({
type: "note-updated",
value: note.value
});
});
The important line is:
channel.postMessage({
type: "note-updated",
value: note.value
});
The postMessage() method sends a message
to the other browser contexts listening on the same channel.
Now open the same page in a second tab.
The second tab still needs to listen for incoming messages.
Receiving updates
We can react to messages using the
message event:
channel.addEventListener("message", event => {
if (event.data.type === "note-updated") {
note.value = event.data.value;
}
});
Now type something in the first tab.
The second tab receives the new value and updates its text area immediately.
The complete JavaScript is still very small:
const channel = new BroadcastChannel("notes");
const note = document.querySelector("#note");
note.addEventListener("input", () => {
channel.postMessage({
type: "note-updated",
value: note.value
});
});
channel.addEventListener("message", event => {
if (event.data.type === "note-updated") {
note.value = event.data.value;
}
});
Open two tabs, edit the note in one of them, and the other follows the changes.
Why send an object?
For such a small example, we could make the code even shorter:
channel.postMessage(note.value);
and receive the value directly:
channel.onmessage = event => {
note.value = event.data;
};
That works perfectly well.
In a real application, however, sending an object gives us more flexibility:
channel.postMessage({
type: "note-updated",
value: note.value
});
Later the same channel could carry other kinds of events:
channel.postMessage({
type: "note-deleted"
});
or:
channel.postMessage({
type: "settings-changed"
});
The channel effectively becomes a very small communication bus between different instances of your web application.
BroadcastChannel doesn't store anything
There is one important detail to understand:
BroadcastChannel is a communication API, not a storage API.
Suppose Tab A contains:
Hello from Tab A
and Tab B is already open.
When Tab A broadcasts the new text, Tab B receives it.
But if you open a new Tab C afterward, that new tab does not automatically receive the previous message.
BroadcastChannel doesn't keep a history of messages.
For example:
note.addEventListener("input", () => {
localStorage.setItem("note", note.value);
channel.postMessage({
type: "note-updated",
value: note.value
});
});
Then initialize the text area when the page loads:
note.value = localStorage.getItem("note") ?? "";
The two APIs now have different responsibilities:
BroadcastChannel
→ live communication between open tabs
localStorage
→ persistence between page loads
This separation is often useful in real applications.
The same-origin rule
BroadcastChannel communication is restricted to the same origin.
For example, two tabs opened on:
https://example.com
can communicate with each other.
A completely different website cannot simply join that application's
"notes" channel.
This is exactly what you normally want when multiple instances of the same web application need to communicate.
Where can this be useful?
A synchronized note is only a simple example.
The same technique can be used whenever one tab needs to tell another that something changed.
For example:
channel.postMessage({
type: "cart-updated"
});
or:
channel.postMessage({
type: "data-invalidated"
});
or even:
channel.postMessage({
type: "document-saved",
id: 123
});
Typical use cases include:
- synchronizing application state
- updating a shopping cart across tabs
- informing other tabs that data has changed
- propagating application settings
- invalidating cached information
- coordinating multiple instances of the same application
Closing the channel
When the channel is no longer needed, it can be closed:
channel.close();
For a channel that exists for the entire lifetime of the page, you may never need to call this manually.
It becomes more useful when channels are created dynamically by components or temporary features.
Final thought
When two instances of a web application need to communicate, it is easy to immediately think about WebSockets, polling or server-side infrastructure.
Those technologies are useful when communication needs to travel through a server.
But if the problem is simply:
How can two tabs of the same web application tell each other that something changed?
the browser already provides a much smaller solution.
Create a channel:
const channel = new BroadcastChannel("notes");
Send a message:
channel.postMessage(data);
And listen for updates:
channel.onmessage = event => {
console.log(event.data);
};
Sometimes synchronizing browser tabs doesn't require additional infrastructure.
It only requires letting the tabs talk to each other.

Comments
Post a Comment