DEV Community

Cover image for I Got Tired of Writing Hacky Cross-Tab Code. So I Built Omnitab.
Anish Bhandarkar
Anish Bhandarkar

Posted on

I Got Tired of Writing Hacky Cross-Tab Code. So I Built Omnitab.

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)
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

🔐 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';
}
Enter fullscreen mode Exit fullscreen mode

🌙 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);
}
Enter fullscreen mode Exit fullscreen mode

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),
  },
});
Enter fullscreen mode Exit fullscreen mode

Get Started

npm install omnitab
Enter fullscreen mode Exit fullscreen mode

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)