notifications api
System notifications for web applications
$ npx docs2skills add web-notifications-apiNotifications API
System notifications for web applications that work outside the browser viewport
What this skill does
The Notifications API enables web applications to display system-level notifications that appear outside the browser's viewport - on the desktop, in the system tray, or mobile notification center. These notifications persist even when users switch tabs or applications, making them ideal for alerts, messages, reminders, and real-time updates.
The API provides permission management, customizable notification content (title, body, icon, actions), and integrates seamlessly with Service Workers for background notifications. It's designed to be compatible with native notification systems across different platforms while maintaining consistent behavior.
Critical for modern web apps that need to re-engage users with timely information, status updates, or urgent alerts when the application isn't actively in focus.
Prerequisites
- HTTPS context (secure origin required)
- Modern browser with Notifications API support
- User interaction required for permission requests
- Service Worker registration (for background notifications)
- No external dependencies or API keys needed
Quick start
// Request permission (must be in response to user gesture)
btn.addEventListener("click", async () => {
const permission = await Notification.requestPermission();
if (permission === "granted") {
// Create basic notification
new Notification("Hello!", {
body: "This is a system notification",
icon: "/icon.png"
});
}
});
Core concepts
Permission Model: Three states - default (not asked), granted (allowed), denied (blocked). Permission persists per origin and can only be requested via user interaction.
Notification Lifecycle: Create → Show → User interaction (click/close) → Events fired. Notifications auto-dismiss after system timeout unless persistent.
Service Worker Integration: Background notifications via ServiceWorkerRegistration.showNotification() persist across page refreshes and enable action buttons with event handling.
Platform Compatibility: Browser translates web notifications to native system notifications, inheriting platform-specific appearance and behavior.
Key API surface
// Permission management
Notification.permission // "default" | "granted" | "denied"
Notification.requestPermission() // Returns Promise<permission>
// Basic notification constructor
new Notification(title, options)
// Service Worker methods
ServiceWorkerRegistration.showNotification(title, options)
ServiceWorkerRegistration.getNotifications(options)
// Notification instance methods/properties
notification.close()
notification.onclick = handler
notification.onshow = handler
notification.onclose = handler
notification.onerror = handler
// Service Worker events
self.addEventListener('notificationclick', event)
self.addEventListener('notificationclose', event)
Common patterns
Basic user notification
async function showBasicNotification() {
if (Notification.permission === "granted") {
new Notification("Task Complete", {
body: "Your file has finished uploading",
icon: "/success-icon.png",
tag: "upload-complete" // Replaces previous with same tag
});
}
}
Permission request flow
async function requestNotificationPermission() {
if (Notification.permission === "default") {
const permission = await Notification.requestPermission();
return permission === "granted";
}
return Notification.permission === "granted";
}
// Usage in click handler
button.onclick = async () => {
const granted = await requestNotificationPermission();
if (granted) {
showNotification();
} else {
showInAppAlert("Notifications blocked");
}
};
Service Worker persistent notifications
// In service worker
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'reply') {
// Handle reply action
clients.openWindow('/messages');
} else {
// Handle default click
clients.openWindow('/');
}
});
// Show from main thread
navigator.serviceWorker.ready.then(registration => {
registration.showNotification("New Message", {
body: "You have a new message from John",
icon: "/message-icon.png",
badge: "/badge-icon.png",
actions: [
{ action: 'reply', title: 'Reply' },
{ action: 'dismiss', title: 'Dismiss' }
],
requireInteraction: true // Won't auto-dismiss
});
});
Rich notification with data
function showRichNotification(messageData) {
const notification = new Notification("New Order #" + messageData.id, {
body: `${messageData.items.length} items - $${messageData.total}`,
icon: "/order-icon.png",
image: messageData.previewImage,
badge: "/badge.png",
tag: `order-${messageData.id}`,
data: messageData, // Attach custom data
requireInteraction: false,
silent: false,
vibrate: [200, 100, 200] // Mobile vibration pattern
});
notification.onclick = () => {
window.focus();
window.location.href = `/orders/${messageData.id}`;
notification.close();
};
}
Background sync notifications
// Service worker background sync
self.addEventListener('sync', event => {
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
async function doBackgroundSync() {
try {
const data = await fetchUpdates();
if (data.hasNewMessages) {
self.registration.showNotification("Updates Available", {
body: `${data.messageCount} new messages`,
tag: "sync-update"
});
}
} catch (error) {
console.error("Background sync failed:", error);
}
}
Configuration
Notification options
{
body: "Notification body text",
icon: "/icon-url.png", // Square icon ~512px
badge: "/badge-url.png", // Monochrome badge ~96px
image: "/large-image.jpg", // Large image for rich notifications
tag: "unique-identifier", // Replaces notifications with same tag
data: { custom: "data" }, // Arbitrary data attached
requireInteraction: false, // Auto-dismiss behavior
silent: false, // Sound/vibration
vibrate: [200, 100, 200], // Vibration pattern (mobile)
timestamp: Date.now(), // Custom timestamp
actions: [ // Action buttons (Service Worker only)
{ action: "action1", title: "Button 1", icon: "/icon1.png" }
]
}
Service Worker registration
// Register service worker first
navigator.serviceWorker.register('/sw.js').then(registration => {
// Use registration.showNotification() for persistent notifications
});
Best practices
- Always check permission before showing notifications: Check
Notification.permissionbefore creating notifications - Request permission only from user gestures: Call
requestPermission()only in click/touch handlers - Use meaningful tags: Prevent notification spam by using tags to replace similar notifications
- Provide fallbacks: Show in-app notifications when permission is denied
- Use Service Worker for persistence: Background notifications survive page refreshes
- Handle all notification events: Implement click, close, and error handlers
- Respect user preferences: Honor
requireInteractionandsilentoptions appropriately - Optimize icon sizes: Use 512x512px for icons, 96x96px for badges
- Test cross-platform: Notification appearance varies by OS/browser
- Implement progressive enhancement: Gracefully degrade when API unavailable
Gotchas and common mistakes
Permission must be requested from user gesture: Calling requestPermission() outside click/touch handlers will fail silently in many browsers. Always tie to user interaction.
HTTPS/localhost only: Notifications API requires secure context. Won't work on HTTP in production, only localhost for development.
Service Worker vs regular notifications behave differently: new Notification() auto-closes and has limited options. ServiceWorkerRegistration.showNotification() persists and supports actions.
Tag replacement is immediate: Notifications with same tag replace previous ones instantly, even if content is identical.
Mobile limitations: iOS Safari has limited support. Android Chrome works well but appearance varies by device.
Action buttons only work with Service Workers: Regular notifications ignore the actions option.
Icons may not display: Many browsers/OS combinations don't show notification icons consistently. Don't rely on them for critical information.
Notification.permission is synchronous: Don't await it - it's a property, not a promise.
Close event timing: onclose fires for both user dismissal and auto-dismiss, making it unreliable for tracking user engagement.
requireInteraction doesn't guarantee persistence: Some platforms ignore this hint and auto-dismiss anyway.
Data serialization: Custom data gets JSON serialized, so functions and complex objects may not survive.
Permission can't be programmatically reset: Once denied, only user can re-enable in browser settings.
Vibration patterns mobile-only: Desktop ignores vibrate option completely.
Badge icons should be monochrome: Colored badges may not display correctly on all platforms.
Notification timing: There's no guaranteed delivery time - system may delay or drop notifications under load.