DEV Community

Cover image for 📺 The BroadcastChannel API — The Browser Feature You've Been Ignoring
Mohamed Ismail
Mohamed Ismail

Posted on

📺 The BroadcastChannel API — The Browser Feature You've Been Ignoring

Most developers find this API by accident — usually while chasing a bug where the user logged out in one tab and the other three still show the dashboard. Embarrassing in dev, security issue in prod.

The fix is built into every modern browser. It's called BroadcastChannel. Most people skip it because they didn't know it was there.


What even is it?

You know how your browser can have the same website open in multiple tabs? Those tabs are completely isolated from each other — they can't talk. Open five tabs of your app and each one lives in its own little bubble.

The BroadcastChannel API fixes that. It lets any tab, window, or iframe on the same origin send a message to all the others. One line to open a channel, one line to send, one line to listen. That's it.

// Open a channel
const channel = new BroadcastChannel('my-app');

// Send a message
channel.postMessage({ type: 'hello', payload: 'world' });

// Listen in another tab
channel.onmessage = (event) => {
  console.log(event.data); // { type: 'hello', payload: 'world' }
};
Enter fullscreen mode Exit fullscreen mode

No libraries. No server. No WebSockets. Just the browser doing the work for free.


01 — Sync logout across tabs

The obvious one, but worth stating. User logs out in tab A — tabs B, C, D don't know. They sit there showing an authenticated UI to nobody.

const authChannel = new BroadcastChannel('auth');

function logout() {
  clearSession();
  authChannel.postMessage({ type: 'LOGOUT' });
  window.location.href = '/login';
}

authChannel.onmessage = (e) => {
  if (e.data.type === 'LOGOUT') {
    window.location.href = '/login';
  }
};
Enter fullscreen mode Exit fullscreen mode

Works both ways — broadcast login too. An idle tab can automatically refresh without the user lifting a finger.


02 — Keep UI state in sync without a round-trip

Power users open your app in two windows side by side. They update something in the left window. The right window is stale. BroadcastChannel pushes that state update to every open tab the moment it changes — no refetch, no polling.

const uiChannel = new BroadcastChannel('ui-state');

function updateTodos(newTodos) {
  setTodos(newTodos);
  uiChannel.postMessage({ type: 'TODOS_UPDATED', todos: newTodos });
}

uiChannel.onmessage = (e) => {
  if (e.data.type === 'TODOS_UPDATED') {
    setTodos(e.data.todos); // works with React, Zustand, anything
  }
};
Enter fullscreen mode Exit fullscreen mode

03 — Single-tab leader election

A polling loop hitting your API every 5 seconds doesn't need to run in all 8 tabs the user has open. That's 8× the server load for zero benefit. Elect one tab as the leader, let it do the work, broadcast results to the rest.

const ch = new BroadcastChannel('leader');
let isLeader = false;
let contested = false;

ch.postMessage({ type: 'CLAIM' });

ch.onmessage = (e) => {
  if (e.data.type === 'CLAIM' && isLeader) {
    ch.postMessage({ type: 'TAKEN' }); // we're already here
  }
  if (e.data.type === 'TAKEN') {
    contested = true; // back off
  }
};

setTimeout(() => {
  if (!contested) {
    isLeader = true;
    startPolling(); // only this tab polls
  }
}, 300);
Enter fullscreen mode Exit fullscreen mode

When the leader tab closes, another tab claims it after the timeout. Users never notice anything happened.


04 — Deduplicate push notifications

Push notification fires from a service worker. You have 3 tabs open. Without coordination, all 3 show the same toast. The user sees the same notification three times. Not great.

// In the service worker — broadcast the push
const ch = new BroadcastChannel('notifications');
self.addEventListener('push', (e) => {
  ch.postMessage({ type: 'PUSH', data: e.data.json() });
});

// In the app — first tab to handle it wins
let handled = false;
ch.onmessage = (e) => {
  if (e.data.type === 'PUSH' && !handled) {
    handled = true;
    showToast(e.data.data);
    setTimeout(() => { handled = false; }, 1000);
  }
};
Enter fullscreen mode Exit fullscreen mode

05 — Theme and settings sync

User flips to dark mode in one tab. Every other tab should switch too — right now. Same for language, font size, accessibility preferences. Anything that should feel global.

const settingsChannel = new BroadcastChannel('user-settings');

function setTheme(theme) {
  localStorage.setItem('theme', theme);
  applyTheme(theme);
  settingsChannel.postMessage({ type: 'THEME', theme });
}

settingsChannel.onmessage = (e) => {
  if (e.data.type === 'THEME') applyTheme(e.data.theme);
};
Enter fullscreen mode Exit fullscreen mode

06 — Share expensive computation results

User uploads a 50MB CSV in Tab A. Your app parses and aggregates it — takes 2 seconds. Tab B is open and needs the same data. Without coordination it parses the whole thing again from scratch. Instead, let Tab A do the work and broadcast the result.

// Tab A — does the heavy lifting
async function processAndShare(file) {
  const result = await parseHugeCSV(file);
  const ch = new BroadcastChannel('processed-data');
  ch.postMessage({ type: 'CSV_READY', data: result });
}

// Tab B — gets it for free
const ch = new BroadcastChannel('processed-data');
ch.onmessage = (e) => {
  if (e.data.type === 'CSV_READY') renderTable(e.data.data);
};
Enter fullscreen mode Exit fullscreen mode

07 — Multi-tab form draft recovery

User starts filling a long form in Tab A, then opens the same route in Tab B by accident. Tab B should detect this and warn: "you have an unsaved draft open in another tab." Saves real data loss, especially in CRMs and ERPs.

const formChannel = new BroadcastChannel('form-lock');

// On mount — check if another tab already has this form open
formChannel.postMessage({ type: 'CHECK_LOCK', formId: 'invoice-42' });

formChannel.onmessage = (e) => {
  if (e.data.type === 'CHECK_LOCK' && e.data.formId === 'invoice-42') {
    formChannel.postMessage({ type: 'LOCKED', formId: 'invoice-42' });
  }
  if (e.data.type === 'LOCKED') {
    showWarning('This form is already open in another tab');
  }
};
Enter fullscreen mode Exit fullscreen mode

08 — Cross-tab undo history

User makes a change in Tab A. Tab B reflects it. Then they hit undo in Tab B. Without coordination — chaos. With BroadcastChannel you can keep a shared undo stack that works across tabs, not just within one.

const historyChannel = new BroadcastChannel('undo-stack');

function pushAction(action) {
  undoStack.push(action);
  historyChannel.postMessage({ type: 'PUSH', action });
}

function undo() {
  undoStack.pop();
  historyChannel.postMessage({ type: 'UNDO' });
  rerender();
}

historyChannel.onmessage = (e) => {
  if (e.data.type === 'PUSH') undoStack.push(e.data.action);
  if (e.data.type === 'UNDO') { undoStack.pop(); rerender(); }
};
Enter fullscreen mode Exit fullscreen mode

09 — Iframe-to-host communication

You're building an embedded widget or microfrontend inside an iframe. window.postMessage works but gets messy fast — origin checks, event filtering, nested frame edge cases. If the host and embed share the same origin, BroadcastChannel is cleaner. Both sides open the same channel and it just works.

// Inside the iframe
const bridge = new BroadcastChannel('widget-bridge');

bridge.postMessage({ type: 'ITEM_SELECTED', id: 42 });

bridge.onmessage = (e) => {
  if (e.data.type === 'RESET') resetWidget();
};
Enter fullscreen mode Exit fullscreen mode

Three things to know before you ship

Same origin only. https://yourapp.com and https://app.yourapp.com are different origins. They cannot talk to each other through this API.

The sender doesn't receive its own message. The tab that calls postMessage won't trigger its own onmessage. If the sender also needs to react, call your update function directly alongside the broadcast.

Clean up in React. Close the channel on unmount — return () => channel.close() inside useEffect. Otherwise you'll leak listeners across re-mounts.


Browser support is all modern browsers — Chrome, Firefox, Safari, Edge, all fine. The whole API is four things: postMessage, onmessage, addEventListener, and close. Once you know it exists, you start seeing the problem it solves in almost every multi-tab web app.

Top comments (0)