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.