If you've ever built a web app where multiple tabs need to stay in sync you know the pain.
The cart updates in one tab, but the other tab has no idea. The user switches themes, but the change doesn't reflect elsewhere. A session expires, but three other open tabs are happily pretending everything is fine.
Every time I hit this problem, I'd write some variation of the same brittle code. localStorage event listeners duct-taped together, inconsistent behavior across browsers, and zero confidence it'd hold up in production. Existing libraries were either overkill for what I needed or quietly broke in Safari.
So I stopped reaching for workarounds and built Omnitab a lightweight, zero-dependency cross-tab messaging library that just works.
The Core Problem
Browser tabs are isolated by design. There's no native "send a message to all my other tabs" API that works everywhere. Instead, there are three different mechanisms - each with their own browser support gaps:
| Transport | How it works | Limitation |
|---|---|---|
| SharedWorker | A shared JS context across tabs | Limited availability |
| BroadcastChannel | Native pub/sub between tabs | Not in IE11 |
| StorageEvent | Piggybacks on localStorage changes | Universal, but indirect |
Most solutions pick one and call it a day. That means you're either dropping Safari users or falling back to something unreliable without knowing it.
How Omnitab Solves It: The Fallback Chain
Omnitab evaluates the browser environment on startup and automatically selects the best available transport:
SharedWorker → BroadcastChannel → StorageEvent
(fastest) (native) (universal)
You don't configure this. You don't think about it. It just picks the right one.
import { createBus } from 'omnitab';
const bus = createBus('my-app');
bus.subscribe('cart:update', (cart) => {
renderCart(cart);
});
bus.publish('cart:update', { items: [...], total: 59.99 });
That's the entire API for basic usage. One function to create a bus, one to publish, one to subscribe.
Real World Use Cases
🛒 Shopping Cart Sync
A user opens your store in two tabs. They add an item in one tab and the other tab should reflect that immediately without a page refresh.
const bus = createBus('shop');
bus.subscribe('cart:update', (cart) => {
renderCart(cart);
});
function updateCart(newCart) {
saveCart(newCart);
bus.publish('cart:update', newCart);
}
🔐 Auth & Session Sync
User clicks "Log out" in one tab. Every other open tab should respond immediately - no stale sessions, no security gaps.
const bus = createBus('auth');
bus.subscribe('auth:logout', () => {
clearLocalSession();
window.location.href = '/login';
});
function logout() {
clearLocalSession();
bus.publish('auth:logout');
window.location.href = '/login';
}
🌙 Theme Sync
User toggles dark mode. All tabs should switch instantly and not just the one they clicked in.
const bus = createBus('ui');
bus.subscribe('theme:change', (theme) => {
document.documentElement.dataset.theme = theme;
});
function setTheme(theme) {
document.documentElement.dataset.theme = theme;
bus.publish('theme:change', theme);
}
Browser Support
Omnitab handles the differences so you don't have to.
| Browser | SharedWorker | BroadcastChannel | StorageEvent |
|---|---|---|---|
| Chrome (desktop) | ✅ | ✅ | ✅ |
| Firefox (desktop) | ✅ | ✅ | ✅ |
| Safari (desktop) | ✅ 16+ | ✅ | ✅ |
| Safari on iOS | ✅ 16+ | ✅ | ✅ |
| Edge | ✅ | ✅ | ✅ |
| Chrome for Android | ❌ | ✅ | ✅ |
| IE11 | ❌ | ❌ | ✅ |
| Samsung Internet | ❌ | ✅ | ✅ |
On initialization, Omnitab logs a console report showing which transport was selected. So you always know what's running under the hood.
Built for Production
Beyond the basics, Omnitab includes features for apps that need reliability:
Health checks : periodically verifies the transport is alive, and reconnects if it goes silent.
Message queue with retry : if a send fails, it queues the message and retries with exponential backoff.
Storage safety : when using the localStorage fallback, Omnitab enforces TTLs, size limits, and configurable eviction policies to prevent runaway storage growth.
const bus = createBus('my-app', {
enableHealthChecks: true,
enableMessageQueue: true,
maxRetries: 5,
retryDelay: 500,
retryBackoff: 2,
worker: {
connectTimeout: 3000,
heartbeatInterval: 5000,
},
storage: {
ttl: 8000,
evictionPolicy: 'oldest',
onStorageFull: (err) => console.warn('Storage full:', err),
},
});
Get Started
npm install omnitab
Full documentation, API reference, and configuration options are on npm.
If you've ever written a hacky localStorage event listener just to sync two tabs then I hope this saves you from writing it again.
Feedback, issues, and contributions are welcome. If you find it useful, a GitHub star goes a long way. 🙏
Top comments (0)