A lightweight React hook for detecting when your application is open in multiple browser tabs.
- ✅ Native
BroadcastChannelAPI for efficient inter-tab communication - ✅ Fully type-safe with strict TypeScript
- ✅ Automatic cleanup of inactive tabs
- ✅ Configurable heartbeat intervals and thresholds
- ✅ Callback support for state changes
- ✅ Browser support detection
- ✅ Zero dependencies (except React peer dependency)
npm install @tabbridge/use-multi-tab-detectionimport { useMultiTabDetection } from '@tabbridge/use-multi-tab-detection';
function App() {
const { isMultiTab, tabCount } = useMultiTabDetection({
channelName: 'my-app'
});
if (isMultiTab) {
return <div>⚠️ Warning: This app is open in {tabCount} tabs</div>;
}
return <div>Your app content</div>;
}Add the hook to your root layout or any component where you want to track multiple tabs:
import { useMultiTabDetection } from '@tabbridge/use-multi-tab-detection';
export default function Layout() {
const { isMultiTab, tabCount, tabId } = useMultiTabDetection({
channelName: 'my-app-channel',
debug: process.env.NODE_ENV === 'development'
});
return (
<div>
{isMultiTab && (
<div className='warning-banner'>
Multiple tabs detected ({tabCount} active)
</div>
)}
{/* Your app content */}
</div>
);
}React to multi-tab state changes with the onMultiTabChange callback:
const { isMultiTab, tabCount, activeTabUrls } = useMultiTabDetection({
channelName: 'my-app',
onMultiTabChange: (isMultiTab, count, urls) => {
console.log(`Multi-tab: ${isMultiTab}, Count: ${count}`);
console.log('Active URLs:', Array.from(urls.values()));
// Send analytics event
if (isMultiTab) {
analytics.track('multi_tab_detected', { tabCount: count });
}
}
});| Option | Type | Default | Description |
|---|---|---|---|
channelName |
string |
Required | Unique identifier for the BroadcastChannel |
heartbeatInterval |
number |
10000 |
Interval (ms) between heartbeat messages |
inactivityThreshold |
number |
30000 |
Time (ms) before considering a tab inactive |
debug |
boolean |
false |
Enable console logging for debugging |
onMultiTabChange |
function |
undefined |
Callback when multi-tab state changes |
(isMultiTab: boolean, tabCount: number, activeTabUrls: Map<string, string>) => void| Property | Type | Description |
|---|---|---|
isMultiTab |
boolean |
Whether multiple tabs are currently detected |
tabCount |
number |
Total count of active tabs |
tabId |
string |
Unique identifier for the current tab |
isSupported |
boolean |
Whether BroadcastChannel API is supported |
activeTabUrls |
Map<string, string> |
Map of tab IDs to their URLs |
- Tab Registration: Each tab creates a unique ID and joins a
BroadcastChannel - Heartbeat System: Tabs send periodic heartbeat messages to announce they're active
- Tab Discovery: New tabs broadcast a discovery message, and existing tabs respond
- Inactive Cleanup: Tabs that haven't sent a heartbeat within the threshold are removed
- State Updates: The hook tracks active tabs and updates state accordingly
Tab 1 Tab 2
| |
|--[heartbeat]---------->|
|<-------[heartbeat]-----|
| |
|--[heartbeat]---------->|
| X (closed)
|
|--[cleanup after 30s]-->
| (detects Tab 2 inactive)
Uses the BroadcastChannel API:
- ✅ Chrome 54+
- ✅ Firefox 38+
- ✅ Safari 15.4+
- ✅ Edge 79+
- ❌ Internet Explorer (not supported)
The hook gracefully handles unsupported browsers by setting isSupported: false.
useMultiTabDetection({
channelName: 'reporting-demo',
onMultiTabChange: async (isMultiTab, count, urls) => {
if (isMultiTab) {
await fetch('/api/report-multi-tab', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tabCount: count,
urls: Array.from(urls.values())
})
});
}
}
});function WarningBanner() {
const { isMultiTab, tabCount } = useMultiTabDetection({
channelName: 'banner-demo'
});
if (!isMultiTab) return null;
return (
<div className='bg-yellow-100 border-l-4 border-yellow-500 p-4'>
<p className='font-bold'>⚠️ Multiple Tabs Detected</p>
<p>
You have {tabCount} tabs open. Please close extra tabs to avoid
synchronization issues.
</p>
</div>
);
}- Open your app in one tab with
debug: trueenabled - Open the same page in a second tab
- Check the browser console for debug messages
- Verify
isMultiTabbecomestrueandtabCountshows2 - Close one tab and verify the count decreases after the inactivity threshold
- Ensure all tabs use the same
channelName - Verify
isSupportedistruein your browser - Enable
debug: trueto see message flow in console - Check browser console for errors
- Increase
inactivityThresholdif you have slow network conditions - Default 30s threshold should work for most cases
- Verify cleanup is running with
debug: true
- Default settings are already optimized for most use cases
- Increase
heartbeatIntervalto reduce message frequency if needed - Avoid triggering expensive operations in
onMultiTabChangecallback - Consider debouncing state updates that cause re-renders
- Triggering global context updates on every state change
- Re-rendering expensive components unnecessarily
- Triggering data refetches in the callback
Best practices:
- Use
onMultiTabChangefor side effects (analytics, logging) - Memoize expensive computations based on
isMultiTab - Debounce UI updates if needed
- BroadcastChannel only communicates within the same origin (browser security)
- Messages cannot be accessed across different domains
- Tab IDs are randomly generated and not tied to user identity
- Avoid sending sensitive data through the channel
We chose BroadcastChannel over alternatives for several reasons:
| Approach | Pros | Cons |
|---|---|---|
| BroadcastChannel | ✅ Native API ✅ No polling ✅ Auto cleanup |
❌ Limited browser support |
| LocalStorage events | ✅ Wider support | ❌ Same-origin only ❌ No auto cleanup |
| SharedWorker | ✅ Powerful | ❌ Complex setup ❌ Limited support |
| Server polling | ✅ Works everywhere | ❌ Network overhead ❌ Requires backend |
This package is written in TypeScript and includes full type definitions:
interface UseMultiTabDetectionOptions {
channelName: string;
heartbeatInterval?: number;
inactivityThreshold?: number;
debug?: boolean;
onMultiTabChange?: (
isMultiTab: boolean,
tabCount: number,
activeTabUrls: Map<string, string>
) => void;
}
interface UseMultiTabDetectionResult {
isMultiTab: boolean;
tabCount: number;
tabId: string;
isSupported: boolean;
activeTabUrls: Map<string, string>;
}Contributions are welcome! Please feel free to submit a Pull Request.
MIT © TabBridge https://github.com/tabbridge
Made with ❤️ by the TabBridge team