JavaScript Notification API: Desktop Push Notifications
Master the Notification API to display native desktop notifications. Learn permission handling, notification options, actions, and best practices for user engagement.
The Notification API allows web applications to display system notifications to users outside the browser window. These notifications can appear even when the browser is minimized or in the background, making them perfect for real-time updates and user engagement.
Understanding the Notification API
The Notification API provides a way to display native desktop notifications that integrate with the user's operating system notification center.
Basic Notification Setup
// Check if notifications are supported
if ('Notification' in window) {
console.log('Notifications are supported');
} else {
console.log('Notifications are not supported');
}
// Request permission
async function requestNotificationPermission() {
if (Notification.permission === 'default') {
const permission = await Notification.requestPermission();
return permission;
}
return Notification.permission;
}
// Create a simple notification
function showNotification() {
if (Notification.permission === 'granted') {
const notification = new Notification('Hello!', {
body: 'This is a notification from your web app',
icon: '/icon-192x192.png',
});
// Handle notification click
notification.onclick = () => {
console.log('Notification clicked');
window.focus();
notification.close();
};
}
}
// Permission states
function checkPermissionState() {
switch (Notification.permission) {
case 'granted':
console.log('Notifications are allowed');
break;
case 'denied':
console.log('Notifications are blocked');
break;
case 'default':
console.log('Permission not yet requested');
break;
}
}
Notification Options
// Full notification options
function createRichNotification() {
const options = {
// Visual options
body: 'This is the notification body text',
icon: '/images/icon-192x192.png',
image: '/images/notification-image.jpg',
badge: '/images/badge-72x72.png',
// Behavior options
tag: 'message-group-1', // Replaces notifications with same tag
renotify: true, // Vibrate/sound even for replacement
requireInteraction: false, // Don't auto-dismiss
silent: false, // Play sound
// Data
data: {
messageId: '123',
timestamp: Date.now(),
},
// Actions (for persistent notifications via service worker)
actions: [
{
action: 'reply',
title: 'Reply',
icon: '/images/reply-icon.png',
},
{
action: 'archive',
title: 'Archive',
icon: '/images/archive-icon.png',
},
],
// Timestamp
timestamp: Date.now(),
// Direction
dir: 'auto', // 'ltr', 'rtl', or 'auto'
// Language
lang: 'en-US',
// Vibration pattern (mobile)
vibrate: [200, 100, 200],
};
const notification = new Notification('New Message', options);
// Event handlers
notification.onshow = () => console.log('Notification shown');
notification.onclick = () => console.log('Notification clicked');
notification.onclose = () => console.log('Notification closed');
notification.onerror = (e) => console.error('Notification error:', e);
return notification;
}
// Notification with custom data
function notificationWithData(title, body, customData) {
const notification = new Notification(title, {
body,
data: customData,
});
notification.onclick = function () {
console.log('Notification data:', this.data);
// Use the custom data
if (this.data.url) {
window.open(this.data.url);
}
this.close();
};
}
// Tagged notifications (replacing previous)
function showTaggedNotification(message, tag) {
new Notification('Chat Message', {
body: message,
tag: tag, // Same tag replaces previous notification
renotify: true, // Alert user about replacement
});
}
Practical Applications
Notification Manager
class NotificationManager {
constructor() {
this.permission = Notification.permission;
this.notifications = new Map();
this.defaultOptions = {
icon: '/images/app-icon.png',
badge: '/images/badge.png',
silent: false,
};
}
async init() {
if (this.permission === 'default') {
this.permission = await Notification.requestPermission();
}
// Monitor permission changes
if ('permissions' in navigator) {
const permissionStatus = await navigator.permissions.query({
name: 'notifications',
});
permissionStatus.onchange = () => {
this.permission = Notification.permission;
this.onPermissionChange(this.permission);
};
}
return this.permission;
}
show(title, options = {}) {
if (this.permission !== 'granted') {
console.warn('Notification permission not granted');
return null;
}
const mergedOptions = { ...this.defaultOptions, ...options };
const notification = new Notification(title, mergedOptions);
// Store notification
const id = Date.now().toString();
this.notifications.set(id, notification);
// Auto-remove after close
notification.onclose = () => {
this.notifications.delete(id);
};
return notification;
}
showWithActions(title, body, actions) {
// Actions require service worker
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.ready.then((registration) => {
registration.showNotification(title, {
body,
actions,
...this.defaultOptions,
});
});
} else {
// Fallback to regular notification
this.show(title, { body });
}
}
closeAll() {
this.notifications.forEach((notification) => {
notification.close();
});
this.notifications.clear();
}
closeByTag(tag) {
this.notifications.forEach((notification, id) => {
if (notification.tag === tag) {
notification.close();
this.notifications.delete(id);
}
});
}
onPermissionChange(permission) {
console.log('Notification permission changed to:', permission);
}
}
// Message notification system
class MessageNotifications {
constructor() {
this.manager = new NotificationManager();
this.conversations = new Map();
}
async init() {
await this.manager.init();
}
showMessage(conversationId, sender, message, avatarUrl) {
const notification = this.manager.show(`${sender}`, {
body: message,
icon: avatarUrl,
tag: `conversation-${conversationId}`,
renotify: true,
data: {
conversationId,
sender,
timestamp: Date.now(),
},
});
if (notification) {
notification.onclick = () => {
this.openConversation(conversationId);
notification.close();
};
// Track conversation notifications
if (!this.conversations.has(conversationId)) {
this.conversations.set(conversationId, []);
}
this.conversations.get(conversationId).push(notification);
}
}
showMessageGroup(conversationId, sender, messages) {
const count = messages.length;
const body = count === 1 ? messages[0] : `${count} new messages`;
this.manager.show(`${sender}`, {
body,
tag: `conversation-${conversationId}`,
badge: '/images/message-badge.png',
data: { conversationId, messages },
});
}
openConversation(conversationId) {
// Navigate to conversation
window.location.href = `/messages/${conversationId}`;
// Clear notifications for this conversation
if (this.conversations.has(conversationId)) {
this.conversations.get(conversationId).forEach((n) => n.close());
this.conversations.delete(conversationId);
}
}
clearConversationNotifications(conversationId) {
this.manager.closeByTag(`conversation-${conversationId}`);
}
}
Task Reminder System
class TaskReminders {
constructor() {
this.notificationManager = new NotificationManager();
this.reminders = new Map();
}
async scheduleReminder(task, reminderTime) {
const now = Date.now();
const delay = reminderTime - now;
if (delay <= 0) {
// Show immediately if time has passed
this.showReminder(task);
return;
}
// Schedule for later
const timerId = setTimeout(() => {
this.showReminder(task);
this.reminders.delete(task.id);
}, delay);
this.reminders.set(task.id, {
timerId,
task,
scheduledFor: reminderTime,
});
return task.id;
}
showReminder(task) {
const notification = this.notificationManager.show('Task Reminder', {
body: task.title,
icon: this.getTaskIcon(task.priority),
tag: `task-${task.id}`,
requireInteraction: task.priority === 'high',
actions: [
{ action: 'complete', title: 'Mark Complete' },
{ action: 'snooze', title: 'Snooze 10 min' },
],
data: { taskId: task.id },
});
if (notification) {
notification.onclick = () => {
this.openTask(task.id);
notification.close();
};
}
}
getTaskIcon(priority) {
const icons = {
high: '/images/priority-high.png',
medium: '/images/priority-medium.png',
low: '/images/priority-low.png',
};
return icons[priority] || icons.medium;
}
snoozeReminder(taskId, minutes = 10) {
const reminder = this.reminders.get(taskId);
if (reminder) {
clearTimeout(reminder.timerId);
const newTime = Date.now() + minutes * 60 * 1000;
this.scheduleReminder(reminder.task, newTime);
}
}
cancelReminder(taskId) {
const reminder = this.reminders.get(taskId);
if (reminder) {
clearTimeout(reminder.timerId);
this.reminders.delete(taskId);
}
}
openTask(taskId) {
window.location.href = `/tasks/${taskId}`;
}
}
Live Update Notifications
// Real-time notification system
class LiveNotifications {
constructor(websocketUrl) {
this.wsUrl = websocketUrl;
this.notificationManager = new NotificationManager();
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
}
async connect() {
await this.notificationManager.init();
if (this.notificationManager.permission !== 'granted') {
console.warn('Notifications not permitted');
return;
}
this.setupWebSocket();
}
setupWebSocket() {
this.ws = new WebSocket(this.wsUrl);
this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectDelay = 1000;
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleNotification(data);
} catch (err) {
console.error('Failed to parse notification:', err);
}
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.scheduleReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
handleNotification(data) {
switch (data.type) {
case 'message':
this.showMessageNotification(data);
break;
case 'alert':
this.showAlertNotification(data);
break;
case 'update':
this.showUpdateNotification(data);
break;
default:
this.showGenericNotification(data);
}
}
showMessageNotification(data) {
const notification = this.notificationManager.show(data.sender, {
body: data.message,
icon: data.senderAvatar,
tag: 'message',
data: { conversationId: data.conversationId },
});
if (notification) {
notification.onclick = () => {
window.focus();
window.location.href = `/messages/${data.conversationId}`;
notification.close();
};
}
}
showAlertNotification(data) {
this.notificationManager.show('Alert', {
body: data.message,
icon: '/images/alert-icon.png',
requireInteraction: true,
vibrate: [200, 100, 200],
});
}
showUpdateNotification(data) {
this.notificationManager.show('Update Available', {
body: data.message,
icon: '/images/update-icon.png',
actions: [
{ action: 'update', title: 'Update Now' },
{ action: 'later', title: 'Later' },
],
});
}
showGenericNotification(data) {
this.notificationManager.show(data.title || 'Notification', {
body: data.body,
icon: data.icon,
});
}
scheduleReconnect() {
setTimeout(() => {
this.setupWebSocket();
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
);
}, this.reconnectDelay);
}
disconnect() {
if (this.ws) {
this.ws.close();
}
}
}
Notification Preferences
// User preference management
class NotificationPreferences {
constructor() {
this.storageKey = 'notification-preferences';
this.defaults = {
enabled: true,
sound: true,
categories: {
messages: true,
reminders: true,
updates: true,
marketing: false,
},
quietHours: {
enabled: false,
start: '22:00',
end: '08:00',
},
};
this.preferences = this.load();
}
load() {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
return { ...this.defaults, ...JSON.parse(stored) };
}
return this.defaults;
}
save() {
localStorage.setItem(this.storageKey, JSON.stringify(this.preferences));
}
get(key) {
return this.preferences[key];
}
set(key, value) {
this.preferences[key] = value;
this.save();
}
setCategoryEnabled(category, enabled) {
if (this.preferences.categories.hasOwnProperty(category)) {
this.preferences.categories[category] = enabled;
this.save();
}
}
isNotificationAllowed(category) {
if (!this.preferences.enabled) return false;
if (!this.preferences.categories[category]) return false;
if (this.isQuietHours()) return false;
return true;
}
isQuietHours() {
if (!this.preferences.quietHours.enabled) return false;
const now = new Date();
const currentTime = now.getHours() * 60 + now.getMinutes();
const [startHour, startMin] = this.preferences.quietHours.start
.split(':')
.map(Number);
const [endHour, endMin] = this.preferences.quietHours.end
.split(':')
.map(Number);
const startTime = startHour * 60 + startMin;
const endTime = endHour * 60 + endMin;
if (startTime <= endTime) {
return currentTime >= startTime && currentTime < endTime;
} else {
// Quiet hours span midnight
return currentTime >= startTime || currentTime < endTime;
}
}
}
// Smart notification controller
class SmartNotifications {
constructor() {
this.manager = new NotificationManager();
this.preferences = new NotificationPreferences();
this.queue = [];
this.rateLimits = new Map();
}
async show(category, title, options) {
// Check preferences
if (!this.preferences.isNotificationAllowed(category)) {
console.log(`Notification blocked by preferences: ${category}`);
return null;
}
// Check rate limiting
if (this.isRateLimited(category)) {
this.queue.push({ category, title, options });
return null;
}
// Update rate limit
this.updateRateLimit(category);
// Show notification
const notification = await this.manager.show(title, {
...options,
silent: !this.preferences.get('sound'),
});
return notification;
}
isRateLimited(category) {
const limit = this.rateLimits.get(category);
if (!limit) return false;
const timeSinceLastNotification = Date.now() - limit.lastShown;
const minInterval = 60000; // 1 minute minimum between same category
return timeSinceLastNotification < minInterval;
}
updateRateLimit(category) {
this.rateLimits.set(category, {
lastShown: Date.now(),
count: (this.rateLimits.get(category)?.count || 0) + 1,
});
}
processQueue() {
const now = Date.now();
this.queue = this.queue.filter((item) => {
if (!this.isRateLimited(item.category)) {
this.show(item.category, item.title, item.options);
return false;
}
return true;
});
}
}
Service Worker Integration
// Service worker notification handling
// In service-worker.js
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
const options = {
body: data.body || 'New notification',
icon: data.icon || '/images/icon-192x192.png',
badge: '/images/badge-72x72.png',
vibrate: [200, 100, 200],
data: data.data || {},
actions: data.actions || [],
tag: data.tag,
requireInteraction: data.requireInteraction || false,
};
event.waitUntil(
self.registration.showNotification(data.title || 'Notification', options)
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action) {
// Handle action clicks
switch (event.action) {
case 'reply':
// Open reply interface
event.waitUntil(
clients.openWindow(
'/messages?reply=' + event.notification.data.messageId
)
);
break;
case 'archive':
// Archive the message
event.waitUntil(
fetch('/api/messages/archive', {
method: 'POST',
body: JSON.stringify({
messageId: event.notification.data.messageId,
}),
})
);
break;
}
} else {
// Handle notification body click
event.waitUntil(
clients.matchAll({ type: 'window' }).then((clientList) => {
// Focus existing window or open new one
for (const client of clientList) {
if (client.url === '/' && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow('/');
}
})
);
}
});
// In main app
class PushNotifications {
async subscribe() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(PUBLIC_VAPID_KEY),
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json',
},
});
}
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;
}
}
Best Practices
- Always check permission before showing notifications
async function safeNotification(title, options) {
if (Notification.permission === 'default') {
await Notification.requestPermission();
}
if (Notification.permission === 'granted') {
return new Notification(title, options);
}
return null;
}
- Provide meaningful notification content
// Good
new Notification('John Doe', {
body: 'Hey, are you available for a quick call?',
icon: '/avatars/john-doe.jpg',
});
// Bad
new Notification('New Message', {
body: 'You have a new message',
});
- Handle notification lifecycle events
function createNotification(title, options) {
const notification = new Notification(title, options);
notification.onshow = () => {
// Track notification shown
analytics.track('notification_shown', { title });
};
notification.onclick = () => {
// Handle click
window.focus();
notification.close();
};
notification.onerror = (error) => {
console.error('Notification error:', error);
// Fallback to in-app notification
showInAppNotification(title, options.body);
};
return notification;
}
- Respect user preferences
class RespectfulNotifications {
constructor() {
this.lastRequestTime = localStorage.getItem('lastNotificationRequest');
this.deniedCount = parseInt(
localStorage.getItem('notificationDeniedCount') || '0'
);
}
async requestPermission() {
// Don't ask too frequently
if (this.shouldSkipRequest()) {
return false;
}
const permission = await Notification.requestPermission();
if (permission === 'denied') {
this.deniedCount++;
localStorage.setItem(
'notificationDeniedCount',
this.deniedCount.toString()
);
}
localStorage.setItem('lastNotificationRequest', Date.now().toString());
return permission === 'granted';
}
shouldSkipRequest() {
// Never ask if denied multiple times
if (this.deniedCount >= 2) return true;
// Don't ask more than once per week
if (this.lastRequestTime) {
const daysSinceLastRequest =
(Date.now() - parseInt(this.lastRequestTime)) / (1000 * 60 * 60 * 24);
return daysSinceLastRequest < 7;
}
return false;
}
}
Conclusion
The Notification API provides a powerful way to engage users with timely, relevant information even when they're not actively using your application. By combining it with service workers for push notifications, proper permission handling, and respect for user preferences, you can create a notification system that enhances user experience without being intrusive. Remember to always prioritize user consent and provide clear value with each notification to maintain trust and engagement.