Concepts to Know

Broadcast Channel API Guide

Complete guide to communicating across browser tabs and windows with BroadcastChannel

The BroadcastChannel API is a simple but powerful way to send messages between browser tabs, windows, iframes, or workers — as long as they share the same origin. It's perfect for syncing state, sending alerts, or keeping things consistent across multiple views of your app.


How It Works

Here's a friendly breakdown:

1. Create a Channel

Give your channel a name:

const channel = new BroadcastChannel('my-dashboard');

All tabs using 'my-dashboard' will be part of the same group.

2. Send a Message

Push messages out with postMessage():

channel.postMessage('User just logged in!');

3. Listen for Messages

Other tabs can listen using the onmessage handler:

channel.onmessage = (event) => {
  console.log('Received:', event.data);
};

Real-World Examples

Multi-Tab Dashboard Sync

Let's say you're building a collaborative app or a multi-tab admin panel.

Tab A (Sender):

class DashboardSender {
  constructor() {
    this.channel = new BroadcastChannel('admin-sync');
  }

  sendNotification(message) {
    this.channel.postMessage({
      type: 'notification',
      message,
      timestamp: Date.now()
    });
  }

  sendDataUpdate(data) {
    this.channel.postMessage({
      type: 'data-update',
      data,
      timestamp: Date.now()
    });
  }

  sendUserAction(action) {
    this.channel.postMessage({
      type: 'user-action',
      action,
      userId: getCurrentUserId(),
      timestamp: Date.now()
    });
  }
}

// Usage
const sender = new DashboardSender();

document.querySelector('#sendBtn').addEventListener('click', () => {
  const msg = document.querySelector('#msgInput').value;
  sender.sendNotification(msg);
});

// Send data updates
sender.sendDataUpdate({
  users: updatedUsers,
  stats: updatedStats
});

Tab B (Receiver):

class DashboardReceiver {
  constructor() {
    this.channel = new BroadcastChannel('admin-sync');
    this.setupListeners();
  }

  setupListeners() {
    this.channel.onmessage = (event) => {
      const { type, message, data, action, userId, timestamp } = event.data;

      switch (type) {
        case 'notification':
          this.showNotification(message);
          break;
        case 'data-update':
          this.updateDashboard(data);
          break;
        case 'user-action':
          this.handleUserAction(action, userId);
          break;
        default:
          console.log('Unknown message type:', type);
      }
    };
  }

  showNotification(message) {
    // Create notification element
    const notification = document.createElement('div');
    notification.className = 'notification';
    notification.textContent = message;
    
    document.body.appendChild(notification);
    
    // Auto-remove after 5 seconds
    setTimeout(() => {
      notification.remove();
    }, 5000);
  }

  updateDashboard(data) {
    // Update UI with new data
    if (data.users) {
      this.updateUserList(data.users);
    }
    if (data.stats) {
      this.updateStats(data.stats);
    }
  }

  handleUserAction(action, userId) {
    console.log(`User ${userId} performed action: ${action}`);
    // Handle specific user actions
  }

  updateUserList(users) {
    // Update user list in UI
    const userList = document.getElementById('user-list');
    if (userList) {
      userList.innerHTML = users.map(user => 
        `<li>${user.name} - ${user.status}</li>`
      ).join('');
    }
  }

  updateStats(stats) {
    // Update statistics in UI
    const statsElement = document.getElementById('stats');
    if (statsElement) {
      statsElement.textContent = `Active Users: ${stats.activeUsers}`;
    }
  }
}

// Initialize receiver
const receiver = new DashboardReceiver();

Advanced Patterns

State Synchronization

class StateSync {
  constructor(channelName = 'app-state') {
    this.channel = new BroadcastChannel(channelName);
    this.state = {};
    this.listeners = new Set();
    this.setupChannel();
  }

  setupChannel() {
    this.channel.onmessage = (event) => {
      const { type, payload, source } = event.data;
      
      if (source === this.channel.name) return; // Avoid echo

      switch (type) {
        case 'state-update':
          this.updateState(payload);
          break;
        case 'state-request':
          this.sendState();
          break;
        case 'state-sync':
          this.syncState(payload);
          break;
      }
    };
  }

  updateState(newState) {
    this.state = { ...this.state, ...newState };
    this.notifyListeners();
  }

  setState(newState) {
    this.state = { ...this.state, ...newState };
    
    // Broadcast to other tabs
    this.channel.postMessage({
      type: 'state-update',
      payload: newState,
      source: this.channel.name
    });

    this.notifyListeners();
  }

  getState() {
    return this.state;
  }

  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  notifyListeners() {
    this.listeners.forEach(listener => listener(this.state));
  }

  sendState() {
    this.channel.postMessage({
      type: 'state-sync',
      payload: this.state,
      source: this.channel.name
    });
  }

  syncState(newState) {
    this.state = newState;
    this.notifyListeners();
  }

  requestState() {
    this.channel.postMessage({
      type: 'state-request',
      source: this.channel.name
    });
  }
}

// Usage
const stateSync = new StateSync('my-app');

// Subscribe to state changes
const unsubscribe = stateSync.subscribe((state) => {
  console.log('State updated:', state);
  updateUI(state);
});

// Update state (will broadcast to other tabs)
stateSync.setState({ user: { name: 'John', isOnline: true } });

// Request state from other tabs
stateSync.requestState();

Authentication Sync

class AuthSync {
  constructor() {
    this.channel = new BroadcastChannel('auth-sync');
    this.setupListeners();
  }

  setupListeners() {
    this.channel.onmessage = (event) => {
      const { type, payload } = event.data;

      switch (type) {
        case 'login':
          this.handleLogin(payload);
          break;
        case 'logout':
          this.handleLogout();
          break;
        case 'token-refresh':
          this.handleTokenRefresh(payload);
          break;
      }
    };
  }

  login(userData) {
    // Store auth data locally
    localStorage.setItem('auth', JSON.stringify(userData));
    
    // Broadcast to other tabs
    this.channel.postMessage({
      type: 'login',
      payload: userData
    });

    this.updateUI(userData);
  }

  logout() {
    // Clear local auth data
    localStorage.removeItem('auth');
    
    // Broadcast to other tabs
    this.channel.postMessage({
      type: 'logout'
    });

    this.updateUI(null);
  }

  handleLogin(userData) {
    // Update UI for login
    this.updateUI(userData);
    this.showNotification('Logged in from another tab');
  }

  handleLogout() {
    // Update UI for logout
    this.updateUI(null);
    this.showNotification('Logged out from another tab');
  }

  handleTokenRefresh(newToken) {
    // Update token
    const auth = JSON.parse(localStorage.getItem('auth') || '{}');
    auth.token = newToken;
    localStorage.setItem('auth', JSON.stringify(auth));
    
    this.showNotification('Token refreshed from another tab');
  }

  updateUI(userData) {
    if (userData) {
      document.body.classList.add('authenticated');
      document.getElementById('user-name').textContent = userData.name;
    } else {
      document.body.classList.remove('authenticated');
      document.getElementById('user-name').textContent = '';
    }
  }

  showNotification(message) {
    // Show notification to user
    console.log(message);
  }
}

// Usage
const authSync = new AuthSync();

// Login (will sync across tabs)
authSync.login({
  name: 'John Doe',
  email: 'john@example.com',
  token: 'jwt-token-here'
});

Form Data Sync

class FormSync {
  constructor(formId, channelName = 'form-sync') {
    this.form = document.getElementById(formId);
    this.channel = new BroadcastChannel(channelName);
    this.setupSync();
  }

  setupSync() {
    // Listen for form changes
    this.form.addEventListener('input', (e) => {
      this.broadcastFormData();
    });

    // Listen for messages from other tabs
    this.channel.onmessage = (event) => {
      const { formData } = event.data;
      this.updateForm(formData);
    };
  }

  broadcastFormData() {
    const formData = this.getFormData();
    this.channel.postMessage({
      type: 'form-update',
      formData
    });
  }

  getFormData() {
    const formData = new FormData(this.form);
    const data = {};
    
    for (let [key, value] of formData.entries()) {
      data[key] = value;
    }
    
    return data;
  }

  updateForm(formData) {
    // Update form fields without triggering events
    Object.entries(formData).forEach(([key, value]) => {
      const field = this.form.querySelector(`[name="${key}"]`);
      if (field && field.value !== value) {
        field.value = value;
        field.classList.add('synced');
        
        // Remove synced class after animation
        setTimeout(() => {
          field.classList.remove('synced');
        }, 1000);
      }
    });
  }
}

// Usage
const formSync = new FormSync('my-form');

Important Considerations

Browser Support and Limitations

// Check for BroadcastChannel support
if (!window.BroadcastChannel) {
  console.warn('BroadcastChannel not supported, falling back to localStorage');
  // Implement fallback using localStorage + storage event
}

// Fallback implementation
class BroadcastChannelFallback {
  constructor(channelName) {
    this.channelName = channelName;
    this.listeners = new Set();
    this.setupStorageListener();
  }

  setupStorageListener() {
    window.addEventListener('storage', (e) => {
      if (e.key === this.channelName) {
        const data = JSON.parse(e.newValue || '{}');
        this.listeners.forEach(listener => listener(data));
      }
    });
  }

  postMessage(data) {
    localStorage.setItem(this.channelName, JSON.stringify({
      data,
      timestamp: Date.now()
    }));
  }

  addEventListener(type, listener) {
    if (type === 'message') {
      this.listeners.add(listener);
    }
  }

  removeEventListener(type, listener) {
    if (type === 'message') {
      this.listeners.delete(listener);
    }
  }

  close() {
    this.listeners.clear();
  }
}

Message Size Limitations

// Check message size before sending
function checkMessageSize(message) {
  const messageSize = new Blob([JSON.stringify(message)]).size;
  const maxSize = 1024 * 1024; // 1MB limit
  
  if (messageSize > maxSize) {
    console.warn('Message too large, consider chunking or compression');
    return false;
  }
  
  return true;
}

// Chunk large messages
function sendLargeMessage(channel, largeData) {
  const chunks = chunkData(largeData, 100000); // 100KB chunks
  
  chunks.forEach((chunk, index) => {
    channel.postMessage({
      type: 'chunked-data',
      chunk,
      index,
      total: chunks.length,
      id: Date.now()
    });
  });
}

function chunkData(data, chunkSize) {
  const string = JSON.stringify(data);
  const chunks = [];
  
  for (let i = 0; i < string.length; i += chunkSize) {
    chunks.push(string.slice(i, i + chunkSize));
  }
  
  return chunks;
}

When to Use It

  • Real-time status updates across tabs
  • Logging a user out in all open tabs
  • Syncing local storage changes
  • Collaborative tools, dashboards, messaging UIs
  • Form data synchronization across tabs
  • Authentication state management
  • App state synchronization

When Not to Use It

  • Communicating across different domains → use postMessage between windows or iframes instead
  • Long-term storage → use localStorage, IndexedDB, or server APIs
  • Large data transfers → consider chunking or compression
  • Frequent updates → consider throttling to avoid performance issues

Performance Best Practices

Message Throttling

class ThrottledBroadcastChannel {
  constructor(channelName, throttleMs = 100) {
    this.channel = new BroadcastChannel(channelName);
    this.throttleMs = throttleMs;
    this.pendingMessages = new Map();
  }

  postMessage(data) {
    const key = JSON.stringify(data);
    
    if (this.pendingMessages.has(key)) {
      // Message already pending, don't send again
      return;
    }

    this.pendingMessages.set(key, setTimeout(() => {
      this.channel.postMessage(data);
      this.pendingMessages.delete(key);
    }, this.throttleMs));
  }
}

Memory Management

// Clean up channels when not needed
function cleanupChannel(channel) {
  if (channel) {
    channel.close();
  }
}

// Clean up on page unload
window.addEventListener('beforeunload', () => {
  cleanupChannel(myChannel);
});

Final Thoughts

The BroadcastChannel API is a lightweight, easy-to-use tool when you want quick communication across tabs. It's great for real-time coordination in modern web apps without the overhead of server-side messaging or complex setups.

Key benefits:

  • Simple API - easy to implement and understand
  • Real-time communication - instant message delivery
  • No server required - works entirely in the browser
  • Cross-tab synchronization - perfect for multi-tab applications

Remember to handle browser support gracefully and consider message size limitations for optimal performance! 🌐💬


Next: ARIA Roles and Attributes - Learn how to implement proper ARIA for custom UI components.