v1.0.0 ~3 KB gzipped TypeScript-first Zero dependencies

TabJS

Cross-tab communication for the modern web — shared state, presence, leader election, locks, duplicate detection, and request/response between every open tab of your app.

Try the playground → GitHub
Duplicate tab detected. Another open tab shares this tab's sessionStorage lineage — perfect for "only one editor" workflows.
FOLLOWER
1
Live Tabs
0
Messages Sent
0
Messages Received
0
Locks Acquired

Live tabs heartbeat

Every tab posts a heartbeat ~every 1.5s. Stale tabs are evicted after 5s. The leader is the oldest live tab — re-elected the moment it leaves.

Shared state synced

Update the counter or theme — every open tab updates in real time. New tabs ask peers for the current state on boot.

Counter
0
Theme

Messaging broadcast / direct / request

Broadcast to every tab, send a private message to one tab, or run a request/response round-trip.

Locks cross-tab mutex

A held lock is broadcast to every tab. Click "Acquire" in two tabs simultaneously — the second one blocks until the first releases.

idle

Event log live

Every cross-tab event in this tab — open, close, focus, leader changes, messages, state, duplicate, locks.

Why TabJS

Everything you need for multi-tab UX

BroadcastChannel where supported, localStorage as a fallback, and a clean typed API on top — so your app behaves predictably no matter how users open it.

🛰️

Presence & heartbeat

Every tab announces itself and posts a heartbeat. Stale tabs are evicted automatically. Subscribe to open, close, focus and blur events.

tabs.on('open', (e) => console.log('+ tab', e.tab.id));
👑

Leader election

The oldest live tab is the leader. When it leaves, a new leader is elected and broadcast — perfect for "only one tab runs the poller".

if (tabs.isLeader) startPolling();
tabs.on('leader', (e) => {/* take over */});
💾

Shared state

One typed state object, synchronized across every open tab. Persisted to localStorage so new tabs hydrate instantly.

tabs.setState({ theme: 'dark' });
tabs.on('state', (e) => render(e.state));
📡

Broadcast & direct

Fire-and-forget broadcasts to every tab, or send a typed message to a single peer by id.

tabs.broadcast('logout');
tabs.send(peerId, 'focus-input');
🤝

Request / response

Promise-based RPC between tabs. Register a handler on one side, await the result on the other.

tabs.handle('double', ({x}) => x*2);
await tabs.request(id, 'double', {x:21});
🔒

Cross-tab locks

A mutex that survives across tabs. Held locks heartbeat so abandoned locks auto-release after a configurable timeout.

await tabs.lock('critical', async () => {
  /* exclusive section */
});
🧬

Duplicate detection

Detect when a user opens "Duplicate tab" — the new tab inherits the same sessionStorage lineage and is flagged instantly.

if (tabs.isDuplicate) showWarning();
tabs.on('duplicate', alertUser);
🚪

Singleton tab

One call to refuse a second tab — redirect, close, or show a "switch to other tab" overlay.

tabs.singleton((others) => {
  location.href = '/already-open';
});
📦

Tiny & framework-agnostic

~3 KB gzipped, zero dependencies, ESM + UMD + types. Works with React, Vue, Svelte, vanilla — any framework, any year.

import { getTabs } from '@buildwithdarsh/tabjs';
$ npm install @buildwithdarsh/tabjs