The Web Notifications API allows web pages to send system notifications to users. This article covers notification API usage and best practices.
Basic Concepts
Checking Support
// Check if notifications are supported
function isNotificationSupported() {
return 'Notification' in window;
}
// Check current permission status
function getNotificationPermission() {
if (!isNotificationSupported()) {
return 'unsupported';
}
return Notification.permission;
// 'default' - not requested
// 'granted' - authorized
// 'denied' - rejected
}
Requesting Permission
// Request notification permission
async function requestNotificationPermission() {
if (!isNotificationSupported()) {
return 'unsupported';
}
// Already granted
if (Notification.permission === 'granted') {
return 'granted';
}
// Already denied (cannot request again)
if (Notification.permission === 'denied') {
return 'denied';
}
// Request permission
try {
const permission = await Notification.requestPermission();
return permission;
} catch (error) {
console.error('Permission request failed:', error);
return 'error';
}
}
// Request on user interaction
document.querySelector('.enable-notifications').addEventListener('click', async () => {
const permission = await requestNotificationPermission();
switch (permission) {
case 'granted':
showToast('Notifications enabled');
break;
case 'denied':
showToast('Notifications denied, please enable in browser settings');
break;
case 'default':
showToast('Please allow notification permission');
break;
}
});
Creating Notifications
Basic Notification
// Create simple notification
function showNotification(title, body) {
if (Notification.permission !== 'granted') {
console.warn('Notifications not authorized');
return null;
}
const notification = new Notification(title, {
body: body
});
return notification;
}
// Usage
showNotification('New Message', 'You have a new message');
Notification Options
// Full notification options
function showRichNotification() {
const notification = new Notification('New Order Alert', {
// Notification body
body: 'You received a new order, click to view details',
// Notification icon
icon: '/icons/notification-icon.png',
// Badge (small icon)
badge: '/icons/badge.png',
// Large image
image: '/images/order-preview.jpg',
// Notification tag (same tag notifications replace each other)
tag: 'order-notification',
// Require user interaction to close
requireInteraction: true,
// Silent mode
silent: false,
// Vibration pattern [vibrate, pause, vibrate]
vibrate: [200, 100, 200],
// Custom data
data: {
orderId: '12345',
url: '/orders/12345'
},
// Text direction
dir: 'ltr', // 'ltr', 'rtl', 'auto'
// Language
lang: 'en-US',
// Timestamp
timestamp: Date.now()
});
return notification;
}
Notification Action Buttons
// Notification with action buttons (requires Service Worker)
async function showActionNotification() {
const registration = await navigator.serviceWorker.ready;
await registration.showNotification('New Message', {
body: 'Message from John: Hello!',
icon: '/icons/message.png',
actions: [
{
action: 'reply',
title: 'Reply',
icon: '/icons/reply.png'
},
{
action: 'dismiss',
title: 'Dismiss',
icon: '/icons/dismiss.png'
}
],
data: {
messageId: '123',
senderId: 'john'
}
});
}
Notification Events
Basic Events
function showNotificationWithEvents(title, options) {
const notification = new Notification(title, options);
// Notification shown
notification.addEventListener('show', () => {
console.log('Notification displayed');
});
// Click notification
notification.addEventListener('click', (event) => {
console.log('Notification clicked');
// Focus window
window.focus();
// Navigate to related page
if (notification.data?.url) {
window.location.href = notification.data.url;
}
// Close notification
notification.close();
});
// Notification closed
notification.addEventListener('close', () => {
console.log('Notification closed');
});
// Notification error
notification.addEventListener('error', (error) => {
console.error('Notification error:', error);
});
return notification;
}
Auto-Close
// Auto-closing notification
function showAutoCloseNotification(title, options, duration = 5000) {
const notification = new Notification(title, options);
// Set auto-close
setTimeout(() => {
notification.close();
}, duration);
return notification;
}
// Usage
showAutoCloseNotification('Download Complete', {
body: 'File downloaded to local storage'
}, 3000);
Service Worker Notifications
Registering 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 successfully');
return registration;
} catch (error) {
console.error('Service Worker registration failed:', error);
return null;
}
}
return null;
}
Handling Notifications in Service Worker
// sw.js
self.addEventListener('notificationclick', (event) => {
const notification = event.notification;
const action = event.action;
const data = notification.data;
notification.close();
// Handle action buttons
if (action === 'reply') {
// Open reply page
event.waitUntil(
clients.openWindow(`/messages/${data.messageId}/reply`)
);
} else if (action === 'dismiss') {
// Mark as read
event.waitUntil(
fetch(`/api/messages/${data.messageId}/dismiss`, {
method: 'POST'
})
);
} else {
// Click on notification body
event.waitUntil(
clients.matchAll({ type: 'window' }).then(clientList => {
// Find open window
for (const client of clientList) {
if (client.url.includes('/messages') && 'focus' in client) {
return client.focus();
}
}
// Open new window
return clients.openWindow(data.url || '/');
})
);
}
});
// Notification close event
self.addEventListener('notificationclose', (event) => {
console.log('Notification closed:', event.notification.tag);
// Can record user behavior
event.waitUntil(
fetch('/api/analytics/notification-closed', {
method: 'POST',
body: JSON.stringify({
tag: event.notification.tag
})
})
);
});
Sending Notifications from Service Worker
// sw.js
self.addEventListener('push', (event) => {
const data = event.data?.json() || {};
const options = {
body: data.body || 'You have a new notification',
icon: data.icon || '/icons/default.png',
badge: '/icons/badge.png',
tag: data.tag || 'default',
data: data,
requireInteraction: data.requireInteraction || false
};
event.waitUntil(
self.registration.showNotification(data.title || 'Notification', options)
);
});
Practical Applications
Message Notification System
class NotificationManager {
constructor() {
this.permission = Notification.permission;
this.unreadCount = 0;
}
async init() {
// Request permission
if (this.permission === 'default') {
this.permission = await Notification.requestPermission();
}
// Listen for messages
this.startListening();
}
startListening() {
// Assume using WebSocket
const ws = new WebSocket('wss://api.example.com/ws');
ws.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
if (message.type === 'new_message') {
this.showMessageNotification(message);
}
});
}
showMessageNotification(message) {
// Only show notification when page is hidden
if (document.hidden && this.permission === 'granted') {
const notification = new Notification(message.sender, {
body: message.content,
icon: message.avatar,
tag: `message-${message.id}`,
data: {
messageId: message.id,
url: `/messages/${message.id}`
}
});
notification.addEventListener('click', () => {
window.focus();
window.location.href = `/messages/${message.id}`;
notification.close();
});
// Update title
this.unreadCount++;
this.updateTitle();
}
}
updateTitle() {
const originalTitle = document.title.replace(/^\(\d+\)\s*/, '');
if (this.unreadCount > 0) {
document.title = `(${this.unreadCount}) ${originalTitle}`;
} else {
document.title = originalTitle;
}
}
clearUnread() {
this.unreadCount = 0;
this.updateTitle();
}
}
// Usage
const notificationManager = new NotificationManager();
notificationManager.init();
// Clear unread when page visible
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
notificationManager.clearUnread();
}
});
Scheduled Reminders
class ReminderService {
constructor() {
this.reminders = new Map();
}
async requestPermission() {
if (Notification.permission !== 'granted') {
await Notification.requestPermission();
}
}
setReminder(id, title, body, time) {
const delay = time - Date.now();
if (delay <= 0) {
console.warn('Reminder time has passed');
return false;
}
// Store timer
const timerId = setTimeout(() => {
this.showReminder(id, title, body);
}, delay);
this.reminders.set(id, {
timerId,
title,
body,
time
});
// Persist storage
this.saveReminders();
return true;
}
showReminder(id, title, body) {
if (Notification.permission === 'granted') {
const notification = new Notification(title, {
body,
icon: '/icons/reminder.png',
tag: `reminder-${id}`,
requireInteraction: true
});
notification.addEventListener('click', () => {
window.focus();
notification.close();
});
}
// Remove reminder
this.reminders.delete(id);
this.saveReminders();
}
cancelReminder(id) {
const reminder = this.reminders.get(id);
if (reminder) {
clearTimeout(reminder.timerId);
this.reminders.delete(id);
this.saveReminders();
return true;
}
return false;
}
saveReminders() {
const data = Array.from(this.reminders.entries()).map(([id, r]) => ({
id,
title: r.title,
body: r.body,
time: r.time
}));
localStorage.setItem('reminders', JSON.stringify(data));
}
loadReminders() {
const data = JSON.parse(localStorage.getItem('reminders') || '[]');
data.forEach(r => {
if (r.time > Date.now()) {
this.setReminder(r.id, r.title, r.body, r.time);
}
});
}
}
Notification Grouping
// Notification group management
class NotificationGroup {
constructor(groupId, maxNotifications = 5) {
this.groupId = groupId;
this.maxNotifications = maxNotifications;
this.notifications = [];
}
add(title, body, data) {
// Close old notifications
if (this.notifications.length >= this.maxNotifications) {
const oldest = this.notifications.shift();
oldest.close();
}
// Create new notification
const notification = new Notification(title, {
body,
tag: `${this.groupId}-${Date.now()}`,
data
});
this.notifications.push(notification);
// Listen for close event
notification.addEventListener('close', () => {
const index = this.notifications.indexOf(notification);
if (index > -1) {
this.notifications.splice(index, 1);
}
});
return notification;
}
closeAll() {
this.notifications.forEach(n => n.close());
this.notifications = [];
}
// Show summary notification
showSummary(count, message) {
this.closeAll();
new Notification(`${count} new messages`, {
body: message,
tag: `${this.groupId}-summary`,
requireInteraction: true
});
}
}
// Usage
const messageGroup = new NotificationGroup('messages', 3);
// On new message
function onNewMessage(message) {
if (messageGroup.notifications.length >= 3) {
messageGroup.showSummary(4, 'You have multiple unread messages');
} else {
messageGroup.add(message.sender, message.content, {
messageId: message.id
});
}
}
Best Practices Summary
Web Notifications Best Practices:
┌─────────────────────────────────────────────────────┐
│ │
│ Permission Handling │
│ ├── Request at appropriate time │
│ ├── Explain why notifications are needed │
│ └── Handle denial gracefully │
│ │
│ Notification Design │
│ ├── Keep it concise and clear │
│ ├── Use meaningful icons │
│ ├── Provide relevant action buttons │
│ └── Avoid excessive notifications │
│ │
│ User Experience │
│ ├── Only send for important events │
│ ├── Provide notification settings │
│ ├── Use tags wisely to avoid duplicates │
│ └── Avoid sending when page is visible │
│ │
└─────────────────────────────────────────────────────┘
| Property | Purpose | Example |
|---|---|---|
| body | Notification body | Message content preview |
| icon | Notification icon | Sender avatar |
| tag | Notification ID | Same tag replaces |
| requireInteraction | Persistent display | Important notifications |
| actions | Action buttons | Reply, Dismiss |
Master the Web Notifications API to create timely and effective user alerts.