javascript-today

Notifications API

What are Browser Notifications?

The Notifications API allows you to display system notifications outside the browser window, even when the page isn’t visible. Perfect for chat apps, reminders, and real-time updates.

if ('Notification' in window) {
  Notification.requestPermission().then(permission => {
    if (permission === 'granted') {
      new Notification('Hello!', {
        body: 'This is a browser notification',
        icon: '/icon.png'
      });
    }
  });
}

Requirements:

  • HTTPS (or localhost)
  • User permission
  • Service Worker (for background notifications)

Checking for Support

function supportsNotifications() {
  return 'Notification' in window;
}

if (supportsNotifications()) {
  console.log('Notifications supported');
} else {
  console.log('Notifications not supported');
  // Show fallback (e.g., in-page alerts)
}

Permission States

Notifications require explicit user permission:

console.log(Notification.permission);
// 'default' - not asked yet
// 'granted' - user allowed
// 'denied' - user blocked

Requesting Permission

Basic Request

async function requestNotificationPermission() {
  if (!('Notification' in window)) {
    console.log('Notifications not supported');
    return 'unsupported';
  }
  
  if (Notification.permission === 'granted') {
    return 'granted';
  }
  
  if (Notification.permission === 'denied') {
    return 'denied';
  }
  
  // Request permission
  const permission = await Notification.requestPermission();
  return permission;
}

// Usage
const permission = await requestNotificationPermission();

if (permission === 'granted') {
  console.log('Permission granted!');
} else {
  console.log('Permission denied');
}

User-Friendly Permission Flow

async function askForNotificationPermission() {
  // Don't ask if already decided
  if (Notification.permission !== 'default') {
    return Notification.permission;
  }
  
  // Show explanation modal first
  const userWants = await showPermissionModal({
    title: 'Enable Notifications',
    message: 'Get notified about new messages and important updates. You can change this anytime in settings.'
  });
  
  if (!userWants) {
    return 'default';
  }
  
  // Request permission
  const permission = await Notification.requestPermission();
  
  if (permission === 'granted') {
    showSuccessMessage('Notifications enabled!');
  } else {
    showInfoMessage('You can enable notifications later in settings');
  }
  
  return permission;
}

Creating Notifications

Basic Notification

function showBasicNotification() {
  if (Notification.permission === 'granted') {
    new Notification('Hello!');
  }
}

With Options

function showNotification() {
  if (Notification.permission === 'granted') {
    const options = {
      body: 'You have 3 new messages',
      icon: '/images/icon.png',
      badge: '/images/badge.png',
      image: '/images/banner.png',
      tag: 'message-notification',  // Unique ID (replaces existing)
      requireInteraction: false,    // Auto-dismiss
      silent: false,                // Play sound
      vibrate: [200, 100, 200],    // Vibration pattern (ms)
      data: { userId: 123 },        // Custom data
      actions: [                     // Action buttons (requires Service Worker)
        { action: 'reply', title: 'Reply' },
        { action: 'close', title: 'Close' }
      ]
    };
    
    const notification = new Notification('New Messages', options);
  }
}

Notification Events

function createInteractiveNotification() {
  const notification = new Notification('New Message', {
    body: 'Alice: Hey, are you there?',
    icon: '/alice-avatar.png',
    tag: 'message-1',
    data: { messageId: 42, sender: 'alice' }
  });
  
  // Clicked
  notification.onclick = (event) => {
    event.preventDefault();  // Prevent default browser behavior
    window.focus();
    openChatWindow(notification.data.sender);
    notification.close();
  };
  
  // Closed
  notification.onclose = () => {
    console.log('Notification closed');
  };
  
  // Error
  notification.onerror = () => {
    console.error('Notification error');
  };
  
  // Shown
  notification.onshow = () => {
    console.log('Notification displayed');
  };
  
  // Auto-close after 5 seconds
  setTimeout(() => {
    notification.close();
  }, 5000);
}

Notification Options

Tag (Replace Existing)

// First notification
new Notification('Downloading...', {
  tag: 'download-status',
  body: '0% complete'
});

// This REPLACES the previous notification (same tag)
setTimeout(() => {
  new Notification('Downloading...', {
    tag: 'download-status',
    body: '50% complete'
  });
}, 2000);

setTimeout(() => {
  new Notification('Download Complete!', {
    tag: 'download-status',
    body: 'Your file is ready'
  });
}, 4000);

Icons and Images

new Notification('Photo Uploaded', {
  body: 'vacation.jpg uploaded successfully',
  icon: '/icon-72x72.png',      // Small icon (next to title)
  badge: '/badge-96x96.png',    // Monochrome badge (Android)
  image: '/vacation-preview.jpg' // Large image (below text)
});

Vibration Pattern

// Vibrate: 200ms on, 100ms off, 200ms on
new Notification('Reminder', {
  body: 'Meeting in 5 minutes',
  vibrate: [200, 100, 200]
});

// Custom pattern
new Notification('Alert!', {
  vibrate: [100, 50, 100, 50, 100, 50, 100]
});

Practical Examples

1. Message Notification System

class MessageNotifier {
  constructor() {
    this.notifications = new Map();
  }
  
  async init() {
    const permission = await requestNotificationPermission();
    return permission === 'granted';
  }
  
  showNewMessage(sender, message) {
    if (Notification.permission !== 'granted') return;
    
    const notification = new Notification(`New message from ${sender}`, {
      body: message,
      icon: `/avatars/${sender}.png`,
      tag: `msg-${sender}`,  // One notification per sender
      requireInteraction: true,
      data: { sender, message }
    });
    
    notification.onclick = () => {
      window.focus();
      this.openChat(sender);
      notification.close();
    };
    
    this.notifications.set(sender, notification);
  }
  
  updateUnreadCount(sender, count) {
    const existingNotification = this.notifications.get(sender);
    
    if (existingNotification) {
      existingNotification.close();
    }
    
    this.showNewMessage(sender, `${count} unread messages`);
  }
  
  clearNotification(sender) {
    const notification = this.notifications.get(sender);
    if (notification) {
      notification.close();
      this.notifications.delete(sender);
    }
  }
  
  openChat(sender) {
    window.location.href = `/chat/${sender}`;
  }
}

// Usage
const notifier = new MessageNotifier();
await notifier.init();

// New message received
notifier.showNewMessage('Alice', 'Hey, are you free?');

// More messages from Alice
notifier.updateUnreadCount('Alice', 3);

2. Reminder System

class ReminderSystem {
  constructor() {
    this.reminders = [];
  }
  
  async addReminder(title, message, delayMs) {
    const id = Date.now();
    
    const timeoutId = setTimeout(() => {
      this.showReminder(id, title, message);
    }, delayMs);
    
    this.reminders.push({ id, title, message, timeoutId });
    
    return id;
  }
  
  showReminder(id, title, message) {
    if (Notification.permission !== 'granted') return;
    
    const notification = new Notification(title, {
      body: message,
      icon: '/reminder-icon.png',
      requireInteraction: true,
      tag: `reminder-${id}`,
      actions: [
        { action: 'snooze', title: 'Snooze 5 min' },
        { action: 'dismiss', title: 'Dismiss' }
      ]
    });
    
    notification.onclick = () => {
      this.handleReminderClick(id);
      notification.close();
    };
    
    // Remove from active reminders
    this.reminders = this.reminders.filter(r => r.id !== id);
  }
  
  handleReminderClick(id) {
    console.log('Reminder clicked:', id);
    window.focus();
  }
  
  cancelReminder(id) {
    const reminder = this.reminders.find(r => r.id === id);
    if (reminder) {
      clearTimeout(reminder.timeoutId);
      this.reminders = this.reminders.filter(r => r.id !== id);
    }
  }
}

// Usage
const reminders = new ReminderSystem();

// Set reminder for 1 minute from now
await reminders.addReminder(
  'Meeting',
  'Team standup in 1 minute',
  60000
);

3. Download Progress

class DownloadNotifier {
  constructor() {
    this.activeDownloads = new Map();
  }
  
  startDownload(fileId, filename) {
    const notification = new Notification('Download Started', {
      body: filename,
      tag: `download-${fileId}`,
      icon: '/download-icon.png'
    });
    
    this.activeDownloads.set(fileId, { notification, filename });
  }
  
  updateProgress(fileId, percent) {
    const download = this.activeDownloads.get(fileId);
    if (!download) return;
    
    // Close old notification
    download.notification.close();
    
    // Show updated progress
    const notification = new Notification('Downloading...', {
      body: `${download.filename} - ${percent}%`,
      tag: `download-${fileId}`,
      icon: '/download-icon.png',
      silent: true  // Don't make sound for progress updates
    });
    
    download.notification = notification;
  }
  
  completeDownload(fileId) {
    const download = this.activeDownloads.get(fileId);
    if (!download) return;
    
    download.notification.close();
    
    const notification = new Notification('Download Complete', {
      body: `${download.filename} is ready`,
      tag: `download-${fileId}`,
      icon: '/success-icon.png',
      requireInteraction: true
    });
    
    notification.onclick = () => {
      this.openDownloadFolder();
      notification.close();
    };
    
    this.activeDownloads.delete(fileId);
  }
  
  openDownloadFolder() {
    // Open downloads folder or file
    console.log('Opening downloads');
  }
}

// Usage
const downloader = new DownloadNotifier();
downloader.startDownload(1, 'vacation-photos.zip');
downloader.updateProgress(1, 25);
downloader.updateProgress(1, 50);
downloader.updateProgress(1, 75);
downloader.completeDownload(1);

4. Real-Time Updates

class UpdateNotifier {
  constructor() {
    this.lastNotificationTime = 0;
    this.minInterval = 60000;  // Minimum 1 minute between notifications
  }
  
  canShowNotification() {
    const now = Date.now();
    return (now - this.lastNotificationTime) >= this.minInterval;
  }
  
  showUpdate(title, message, type = 'info') {
    if (Notification.permission !== 'granted') return;
    if (!this.canShowNotification()) return;  // Rate limiting
    
    const icons = {
      info: '/info-icon.png',
      warning: '/warning-icon.png',
      error: '/error-icon.png',
      success: '/success-icon.png'
    };
    
    const notification = new Notification(title, {
      body: message,
      icon: icons[type] || icons.info,
      tag: `update-${type}`,
      requireInteraction: type === 'error'
    });
    
    this.lastNotificationTime = Date.now();
    
    return notification;
  }
  
  showSystemUpdate(version) {
    return this.showUpdate(
      'Update Available',
      `Version ${version} is ready to install`,
      'info'
    );
  }
  
  showError(message) {
    return this.showUpdate(
      'Error Occurred',
      message,
      'error'
    );
  }
}

// Usage
const updater = new UpdateNotifier();
updater.showSystemUpdate('2.0.0');

Service Worker Notifications

For persistent notifications (work even when page is closed):

// In your main page
async function showPersistentNotification() {
  if ('serviceWorker' in navigator) {
    const registration = await navigator.serviceWorker.ready;
    
    await registration.showNotification('Persistent Notification', {
      body: 'This works even if the page is closed',
      icon: '/icon.png',
      actions: [
        { action: 'open', title: 'Open App' },
        { action: 'dismiss', title: 'Dismiss' }
      ],
      tag: 'persistent-notification'
    });
  }
}

// In service-worker.js
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  
  if (event.action === 'open') {
    event.waitUntil(
      clients.openWindow('/')
    );
  }
});

Best Practices

DO:

// Check permission before showing
if (Notification.permission === 'granted') {
  new Notification('Message');
}

// Use tags to replace old notifications
new Notification('Status', {
  tag: 'status-update',  // Replaces previous with same tag
  body: 'Processing...'
});

// Provide context in notification
new Notification('New Message', {
  body: 'Alice: Hey there!',
  icon: '/alice-avatar.png',
  data: { senderId: 123 }  // For click handling
});

// Handle clicks
notification.onclick = () => {
  window.focus();
  notification.close();
};

// Rate limit notifications
const lastNotification = Date.now();
if (Date.now() - lastNotification > 60000) {
  showNotification();
}

// Close notifications when done
notification.close();

DON’T:

// Don't spam users
for (let i = 0; i < 100; i++) {
  new Notification(`Message ${i}`);  // ❌ Annoying!
}

// Don't show without permission
new Notification('Hi');  // ❌ Check permission first

// Don't show notifications for everything
new Notification('Mouse moved');  // ❌ Irrelevant

// Don't forget to require HTTPS
// http://example.com  ❌

// Don't use vague messages
new Notification('Update');  // ❌ Update about what?

// Don't leave notifications open forever
// Always close or set requireInteraction appropriately

Notification Patterns

Grouping Multiple Notifications

function showGroupedNotifications(messages) {
  if (messages.length === 1) {
    new Notification(messages[0].sender, {
      body: messages[0].text
    });
  } else {
    new Notification(`${messages.length} New Messages`, {
      body: messages.map(m => `${m.sender}: ${m.text}`).join('\n'),
      tag: 'grouped-messages'
    });
  }
}

Silent Background Updates

new Notification('Background sync complete', {
  body: 'Your data is up to date',
  silent: true,  // No sound
  tag: 'bg-sync',
  requireInteraction: false  // Auto-dismiss
});

Summary

Check Support:

if ('Notification' in window) {
  // Notifications supported
}

Request Permission:

const permission = await Notification.requestPermission();
// 'granted', 'denied', or 'default'

Show Notification:

if (Notification.permission === 'granted') {
  const notification = new Notification('Title', {
    body: 'Message text',
    icon: '/icon.png',
    tag: 'unique-id',
    data: { customData: 'value' }
  });
  
  notification.onclick = () => {
    window.focus();
    notification.close();
  };
}

Key Options:

  • body - Message text
  • icon - Small icon image
  • tag - Unique ID (replaces existing)
  • requireInteraction - Don’t auto-dismiss
  • silent - No sound
  • vibrate - Vibration pattern
  • data - Custom data for events

Events:

  • onclick - User clicked
  • onclose - Notification closed
  • onerror - Error occurred
  • onshow - Notification displayed

Requirements:

  • HTTPS (or localhost)
  • User permission
  • Modern browser