Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Your Tabs Can Talk to Each Other: BroadcastChannel

C
Carlo Fontanos
· 2 min read

A user files a bug: "I logged out, but another tab still showed me logged in, and I placed an order as... myself?" Multi-tab inconsistency is one of those problems everyone has and few explicitly handle - partly because people don't know the fix is three lines.

const channel = new BroadcastChannel('app-state');

// Tab that logs out:
channel.postMessage({ type: 'logout' });

// Every OTHER tab on the same origin:
channel.onmessage = (e) => {
    if (e.data.type === 'logout') location.href = '/login';
};

Same-origin tabs, windows, iframes and even workers subscribed to the same channel name receive the message. Payloads go through structured clone, so objects, Dates and Maps survive - same algorithm as structuredClone. The sender never receives its own message, which conveniently prevents echo loops.

What I actually sync with it

// Theme toggle - all tabs flip together
channel.postMessage({ type: 'theme', value: 'dark' });

// Cart updates - badge count stays right everywhere
channel.postMessage({ type: 'cart', count: cart.length });

// "This record is being edited in another tab" soft locks
channel.postMessage({ type: 'editing', recordId: 42 });

That last one is underrated for admin panels: a heartbeat message every few seconds while a form is open, and other tabs show "also open elsewhere" instead of letting two copies of the same record fight.

The legacy route: the storage event

Before BroadcastChannel, cross-tab messaging was a localStorage hack that still works and still matters for old Safari:

// Sender
localStorage.setItem('broadcast', JSON.stringify({ type: 'logout', at: Date.now() }));

// Receivers - fires in every tab EXCEPT the one that wrote
addEventListener('storage', (e) => {
    if (e.key === 'broadcast') handle(JSON.parse(e.newValue));
});

Two quirks define it: the event only fires in other tabs (a feature, same as BroadcastChannel), and it only fires when the value actually changes - hence the Date.now() salt so repeated identical messages still deliver. It's clunkier (strings only, pollutes storage), but it reaches back a decade.

Notes from production

  • Messages are fire-and-forget to currently open listeners - a tab that opens later hears nothing. Pair the channel with persisted state (localStorage/cookie) for late joiners: read state on load, listen for changes after.
  • Close channels you create in components (channel.close()) or the listener keeps the context alive.
  • Support: everywhere since Safari finally shipped it in 15.4 (2022). For anything older, the storage event is the graceful path.

For logout specifically, do both: broadcast for instant UI response, and validate the session server-side on the next request anyway - client-side sync is UX, not security.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading