JavaScript Notification API Complete Guide

Master browser notifications: permission requests, message pushing, custom options, and Service Worker integration

JavaScript Notification API Complete Guide

The Notification API allows web pages to send system notifications to users. This article covers its usage and best practices.

Basic Usage

Requesting Permission

// Check notification support
if (!('Notification' in window)) {
  console.log('Browser does not support notifications');
}

// Check current permission status
console.log(Notification.permission);
// 'default' - Not decided
// 'granted' - Authorized
// 'denied' - Rejected

// Request permission
async function requestNotificationPermission() {
  try {
    const permission = await Notification.requestPermission();
    
    if (permission === 'granted') {
      console.log('Notification permission granted');
      return true;
    } else if (permission === 'denied') {
      console.log('Notification permission denied');
      return false;
    } else {
      console.log('User did not decide');
      return false;
    }
  } catch (error) {
    console.error('Failed to request permission:', error);
    return false;
  }
}

// Callback method (legacy compatibility)
Notification.requestPermission(function(permission) {
  console.log('Permission status:', permission);
});

Creating Basic Notifications

// Simple notification
function showNotification(title, options = {}) {
  if (Notification.permission !== 'granted') {
    console.warn('Notification permission not granted');
    return null;
  }
  
  return new Notification(title, options);
}

// Usage example
const notification = showNotification('New Message', {
  body: 'You have a new message',
  icon: '/icons/message.png'
});

Notification Options

const notification = new Notification('Notification Title', {
  // Body content
  body: 'This is the detailed content of the notification',
  
  // Icon
  icon: '/icons/notification.png',
  
  // Badge (small icon)
  badge: '/icons/badge.png',
  
  // Image (large)
  image: '/images/preview.jpg',
  
  // Tag (notifications with same tag will replace)
  tag: 'message-group-1',
  
  // Whether to replace notifications with same tag
  renotify: true,
  
  // Requires user interaction to close
  requireInteraction: true,
  
  // Silent mode
  silent: false,
  
  // Vibration pattern [vibrate, pause, vibrate, ...]
  vibrate: [200, 100, 200],
  
  // Timestamp
  timestamp: Date.now(),
  
  // Custom data
  data: {
    url: 'https://example.com/message/123',
    messageId: 123
  },
  
  // Text direction
  dir: 'auto', // 'ltr', 'rtl', 'auto'
  
  // Language
  lang: 'en-US',
  
  // Action buttons (requires Service Worker)
  actions: [
    { action: 'reply', title: 'Reply', icon: '/icons/reply.png' },
    { action: 'archive', title: 'Archive', icon: '/icons/archive.png' }
  ]
});

Notification Events

const notification = new Notification('New Message', {
  body: 'Click for details'
});

// When notification is shown
notification.onshow = (event) => {
  console.log('Notification shown');
};

// When notification is clicked
notification.onclick = (event) => {
  event.preventDefault();
  window.focus();
  window.location.href = '/messages';
  notification.close();
};

// When notification is closed
notification.onclose = (event) => {
  console.log('Notification closed');
};

// When notification errors
notification.onerror = (event) => {
  console.error('Notification error:', event);
};

// Manually close notification
setTimeout(() => {
  notification.close();
}, 5000);

Notification Manager

Wrapper Class

class NotificationManager {
  constructor() {
    this.supported = 'Notification' in window;
    this.notifications = new Map();
  }
  
  async requestPermission() {
    if (!this.supported) {
      throw new Error('Browser does not support notifications');
    }
    
    if (Notification.permission === 'granted') {
      return true;
    }
    
    if (Notification.permission === 'denied') {
      return false;
    }
    
    const permission = await Notification.requestPermission();
    return permission === 'granted';
  }
  
  get permission() {
    return this.supported ? Notification.permission : 'denied';
  }
  
  show(title, options = {}) {
    if (this.permission !== 'granted') {
      console.warn('Notification permission not granted');
      return null;
    }
    
    const id = options.tag || crypto.randomUUID();
    
    const notification = new Notification(title, {
      ...options,
      tag: id
    });
    
    this.notifications.set(id, notification);
    
    notification.onclose = () => {
      this.notifications.delete(id);
      options.onClose?.();
    };
    
    notification.onclick = (event) => {
      options.onClick?.(event);
    };
    
    // Auto close
    if (options.autoClose !== false) {
      const timeout = options.autoClose || 5000;
      setTimeout(() => {
        notification.close();
      }, timeout);
    }
    
    return id;
  }
  
  close(id) {
    const notification = this.notifications.get(id);
    if (notification) {
      notification.close();
      this.notifications.delete(id);
    }
  }
  
  closeAll() {
    this.notifications.forEach(notification => {
      notification.close();
    });
    this.notifications.clear();
  }
}

// Usage
const notificationManager = new NotificationManager();

async function init() {
  const granted = await notificationManager.requestPermission();
  
  if (granted) {
    notificationManager.show('Welcome', {
      body: 'Thanks for enabling notifications',
      onClick: () => {
        console.log('Notification clicked');
      }
    });
  }
}

Notification Queue

class NotificationQueue {
  constructor(options = {}) {
    this.maxConcurrent = options.maxConcurrent || 3;
    this.queue = [];
    this.active = new Set();
    this.manager = new NotificationManager();
  }
  
  async enqueue(title, options = {}) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        title,
        options,
        resolve,
        reject
      });
      
      this.process();
    });
  }
  
  process() {
    while (this.active.size < this.maxConcurrent && this.queue.length > 0) {
      const item = this.queue.shift();
      this.showNext(item);
    }
  }
  
  showNext(item) {
    const { title, options, resolve, reject } = item;
    
    try {
      const id = this.manager.show(title, {
        ...options,
        onClose: () => {
          this.active.delete(id);
          options.onClose?.();
          this.process();
        }
      });
      
      if (id) {
        this.active.add(id);
        resolve(id);
      } else {
        reject(new Error('Failed to create notification'));
      }
    } catch (error) {
      reject(error);
    }
  }
  
  clear() {
    this.queue = [];
    this.manager.closeAll();
    this.active.clear();
  }
}

// Usage
const queue = new NotificationQueue({ maxConcurrent: 2 });

// Send batch notifications
async function sendBatchNotifications(messages) {
  for (const msg of messages) {
    await queue.enqueue(msg.title, {
      body: msg.body,
      autoClose: 3000
    });
  }
}

Service Worker Integration

Register Service Worker

// main.js
async function registerServiceWorker() {
  if ('serviceWorker' in navigator) {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('Service Worker registered:', registration.scope);
      return registration;
    } catch (error) {
      console.error('Service Worker registration failed:', error);
      throw error;
    }
  }
  throw new Error('Service Worker not supported');
}

Notifications in Service Worker

// sw.js
self.addEventListener('push', (event) => {
  const data = event.data?.json() || {};
  
  const options = {
    body: data.body || 'New message',
    icon: data.icon || '/icons/default.png',
    badge: '/icons/badge.png',
    tag: data.tag || 'default',
    data: data.payload || {},
    actions: [
      { action: 'open', title: 'Open' },
      { action: 'dismiss', title: 'Dismiss' }
    ],
    requireInteraction: true
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title || 'Notification', options)
  );
});

// Handle notification click
self.addEventListener('notificationclick', (event) => {
  const notification = event.notification;
  const action = event.action;
  const data = notification.data;
  
  notification.close();
  
  if (action === 'open' || !action) {
    // Open or focus window
    event.waitUntil(
      clients.matchAll({ type: 'window', includeUncontrolled: true })
        .then(clientList => {
          // Find already open window
          for (const client of clientList) {
            if (client.url === data.url && 'focus' in client) {
              return client.focus();
            }
          }
          // Open new window
          if (clients.openWindow) {
            return clients.openWindow(data.url || '/');
          }
        })
    );
  } else if (action === 'dismiss') {
    // Dismiss
    console.log('Notification dismissed');
  }
});

// Handle notification close
self.addEventListener('notificationclose', (event) => {
  const notification = event.notification;
  console.log('Notification closed:', notification.tag);
});

Push Subscription

// main.js
async function subscribeToPush(registration) {
  try {
    // Check if already subscribed
    let subscription = await registration.pushManager.getSubscription();
    
    if (subscription) {
      console.log('Subscription already exists');
      return subscription;
    }
    
    // Get server public key
    const response = await fetch('/api/push/vapid-public-key');
    const vapidPublicKey = await response.text();
    
    // Convert public key format
    const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
    
    // Create subscription
    subscription = await registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey
    });
    
    // Send subscription to server
    await fetch('/api/push/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(subscription)
    });
    
    console.log('Push subscription successful');
    return subscription;
  } catch (error) {
    console.error('Push subscription failed:', error);
    throw error;
  }
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');
  
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  
  return outputArray;
}

Practical Applications

Message Notification System

class MessageNotificationSystem {
  constructor() {
    this.manager = new NotificationManager();
    this.unreadCount = 0;
    this.enabled = this.loadPreference();
  }
  
  loadPreference() {
    return localStorage.getItem('notifications') !== 'disabled';
  }
  
  savePreference(enabled) {
    localStorage.setItem('notifications', enabled ? 'enabled' : 'disabled');
    this.enabled = enabled;
  }
  
  async enable() {
    const granted = await this.manager.requestPermission();
    if (granted) {
      this.savePreference(true);
    }
    return granted;
  }
  
  disable() {
    this.savePreference(false);
    this.manager.closeAll();
  }
  
  notify(message) {
    if (!this.enabled || this.manager.permission !== 'granted') {
      return;
    }
    
    // Don't show notification when page is visible
    if (document.visibilityState === 'visible') {
      return;
    }
    
    this.unreadCount++;
    
    this.manager.show(message.sender, {
      body: message.text,
      icon: message.avatar,
      tag: `message-${message.id}`,
      data: { messageId: message.id },
      onClick: () => {
        window.focus();
        this.openMessage(message.id);
      }
    });
    
    this.updateBadge();
  }
  
  openMessage(messageId) {
    // Navigate to message
    window.location.href = `/messages/${messageId}`;
  }
  
  updateBadge() {
    if ('setAppBadge' in navigator) {
      navigator.setAppBadge(this.unreadCount);
    }
  }
  
  clearBadge() {
    this.unreadCount = 0;
    if ('clearAppBadge' in navigator) {
      navigator.clearAppBadge();
    }
  }
}

// Usage
const messageSystem = new MessageNotificationSystem();

// When receiving new message
socket.on('newMessage', (message) => {
  messageSystem.notify(message);
});

Reminder System

class ReminderSystem {
  constructor() {
    this.reminders = this.loadReminders();
    this.timers = new Map();
    this.manager = new NotificationManager();
    
    this.scheduleAll();
  }
  
  loadReminders() {
    try {
      return JSON.parse(localStorage.getItem('reminders')) || [];
    } catch {
      return [];
    }
  }
  
  saveReminders() {
    localStorage.setItem('reminders', JSON.stringify(this.reminders));
  }
  
  add(reminder) {
    const id = crypto.randomUUID();
    const newReminder = {
      id,
      title: reminder.title,
      body: reminder.body,
      time: reminder.time,
      repeat: reminder.repeat || null
    };
    
    this.reminders.push(newReminder);
    this.saveReminders();
    this.schedule(newReminder);
    
    return id;
  }
  
  remove(id) {
    const timer = this.timers.get(id);
    if (timer) {
      clearTimeout(timer);
      this.timers.delete(id);
    }
    
    this.reminders = this.reminders.filter(r => r.id !== id);
    this.saveReminders();
  }
  
  schedule(reminder) {
    const now = Date.now();
    const time = new Date(reminder.time).getTime();
    const delay = time - now;
    
    if (delay <= 0) {
      // Already expired
      if (!reminder.repeat) {
        this.remove(reminder.id);
      }
      return;
    }
    
    const timer = setTimeout(() => {
      this.trigger(reminder);
    }, delay);
    
    this.timers.set(reminder.id, timer);
  }
  
  scheduleAll() {
    this.reminders.forEach(reminder => {
      this.schedule(reminder);
    });
  }
  
  trigger(reminder) {
    this.manager.show(reminder.title, {
      body: reminder.body,
      tag: `reminder-${reminder.id}`,
      requireInteraction: true,
      actions: [
        { action: 'snooze', title: 'Snooze' },
        { action: 'dismiss', title: 'Dismiss' }
      ]
    });
    
    // Handle recurring reminders
    if (reminder.repeat) {
      const nextTime = this.calculateNextTime(reminder);
      reminder.time = nextTime;
      this.saveReminders();
      this.schedule(reminder);
    } else {
      this.remove(reminder.id);
    }
  }
  
  calculateNextTime(reminder) {
    const current = new Date(reminder.time);
    
    switch (reminder.repeat) {
      case 'daily':
        current.setDate(current.getDate() + 1);
        break;
      case 'weekly':
        current.setDate(current.getDate() + 7);
        break;
      case 'monthly':
        current.setMonth(current.getMonth() + 1);
        break;
    }
    
    return current.toISOString();
  }
}

// Usage
const reminders = new ReminderSystem();

// Add reminder
reminders.add({
  title: 'Meeting Reminder',
  body: 'Team meeting in 15 minutes',
  time: new Date(Date.now() + 15 * 60 * 1000).toISOString()
});

// Add daily reminder
reminders.add({
  title: 'Daily Standup',
  body: 'Get ready for daily standup',
  time: '2025-01-29T09:00:00',
  repeat: 'daily'
});

Download Progress Notification

class DownloadNotifier {
  constructor() {
    this.manager = new NotificationManager();
    this.downloads = new Map();
  }
  
  start(downloadId, filename) {
    const notificationId = this.manager.show('Download Started', {
      body: filename,
      tag: `download-${downloadId}`,
      requireInteraction: true,
      autoClose: false
    });
    
    this.downloads.set(downloadId, {
      filename,
      notificationId,
      progress: 0
    });
  }
  
  updateProgress(downloadId, progress) {
    const download = this.downloads.get(downloadId);
    if (!download) return;
    
    download.progress = progress;
    
    // Update notification
    this.manager.show('Downloading...', {
      body: `${download.filename} - ${progress}%`,
      tag: `download-${downloadId}`,
      requireInteraction: true,
      autoClose: false
    });
  }
  
  complete(downloadId) {
    const download = this.downloads.get(downloadId);
    if (!download) return;
    
    this.manager.show('Download Complete', {
      body: download.filename,
      tag: `download-${downloadId}`,
      autoClose: 5000,
      onClick: () => {
        // Open downloads folder
        console.log('Open downloaded file');
      }
    });
    
    this.downloads.delete(downloadId);
  }
  
  error(downloadId, errorMessage) {
    const download = this.downloads.get(downloadId);
    if (!download) return;
    
    this.manager.show('Download Failed', {
      body: `${download.filename}: ${errorMessage}`,
      tag: `download-${downloadId}`,
      autoClose: 10000
    });
    
    this.downloads.delete(downloadId);
  }
}

// Usage
const downloadNotifier = new DownloadNotifier();

async function downloadFile(url, filename) {
  const downloadId = crypto.randomUUID();
  downloadNotifier.start(downloadId, filename);
  
  try {
    const response = await fetch(url);
    const reader = response.body.getReader();
    const contentLength = +response.headers.get('Content-Length');
    
    let receivedLength = 0;
    const chunks = [];
    
    while (true) {
      const { done, value } = await reader.read();
      
      if (done) break;
      
      chunks.push(value);
      receivedLength += value.length;
      
      const progress = Math.round((receivedLength / contentLength) * 100);
      downloadNotifier.updateProgress(downloadId, progress);
    }
    
    downloadNotifier.complete(downloadId);
    
    // Process downloaded data...
    return new Blob(chunks);
  } catch (error) {
    downloadNotifier.error(downloadId, error.message);
    throw error;
  }
}

Best Practices Summary

Notification API Best Practices:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   Permission Management                             │
│   ├── Explain why requesting permission            │
│   ├── Request after user action                    │
│   ├── Handle permission denial gracefully          │
│   └── Provide option to disable notifications      │
│                                                     │
│   User Experience                                   │
│   ├── Only send valuable notifications             │
│   ├── Use tag to consolidate related ones          │
│   ├── Avoid notifications when page visible        │
│   └── Provide notification preferences             │
│                                                     │
│   Technical Implementation                          │
│   ├── Use Service Worker for offline push          │
│   ├── Handle notification click properly           │
│   ├── Set reasonable auto-close time               │
│   └── Use actions for quick responses              │
│                                                     │
└─────────────────────────────────────────────────────┘
Permission StatusDescriptionHandling
defaultNot decidedCan request permission
grantedAuthorizedCan send notifications
deniedRejectedGuide user to enable manually

Use the Notification API wisely to deliver valuable information at the right time.