Notifications & Permissions API
Introduction to the Notification API
The Notification API allows web applications to send notifications to users outside the context of the browser tab. These notifications appear at the system level -- in the operating system's notification center on desktops or as push-style alerts on mobile devices. They are essential for keeping users informed about important events such as new messages, task reminders, real-time updates, and background process completions even when the user is not actively looking at your application.
The Notification API is a browser-native feature that requires explicit user permission before any notifications can be displayed. This permission model is a critical part of the web platform's security architecture, ensuring that websites cannot spam users with unwanted alerts. Understanding how to properly request, check, and handle permissions is just as important as knowing how to construct and display notifications.
Checking Notification Support
Before attempting to use the Notification API, you should always verify that the browser supports it. The Notification object is available on the global window scope in supporting browsers.
Example: Checking for Notification API Support
function isNotificationSupported() {
if (!("Notification" in window)) {
console.log("This browser does not support notifications.");
return false;
}
console.log("Notifications are supported!");
return true;
}
// Usage
if (isNotificationSupported()) {
// Proceed with notification logic
console.log("Current permission:", Notification.permission);
}
Understanding Permission States
The Notification API uses a three-state permission model that determines whether your application can display notifications. These states are accessible through Notification.permission and are fundamental to building a good notification experience.
- default -- The user has not yet made a decision. You can request permission, and the browser will show a prompt asking the user to allow or block notifications.
- granted -- The user has explicitly allowed notifications. You can create and display notifications freely.
- denied -- The user has explicitly blocked notifications. You cannot show notifications, and you cannot re-prompt the user. They must manually change this setting in their browser preferences.
Example: Checking the Current Permission State
function checkNotificationPermission() {
switch (Notification.permission) {
case "granted":
console.log("Permission granted. Notifications can be shown.");
return true;
case "denied":
console.log("Permission denied. User must enable in browser settings.");
return false;
case "default":
console.log("Permission not yet requested. Will prompt user.");
return null;
default:
console.log("Unknown permission state.");
return false;
}
}
const status = checkNotificationPermission();
Notification.requestPermission(). The user must manually navigate to their browser settings to re-enable notifications for your site. Design your user experience to explain the value of notifications before requesting permission, and handle the denied state gracefully.Requesting Permission
Permission is requested using the Notification.requestPermission() method. This method returns a Promise that resolves with the user's choice. Modern best practice is to use the Promise-based syntax, though an older callback-based syntax also exists for backward compatibility.
Example: Requesting Notification Permission (Promise-based)
async function requestNotificationPermission() {
if (!("Notification" in window)) {
console.log("Notifications not supported.");
return "unsupported";
}
// If already granted, no need to ask again
if (Notification.permission === "granted") {
console.log("Permission already granted.");
return "granted";
}
// If denied, cannot re-ask
if (Notification.permission === "denied") {
console.log("Permission was previously denied.");
return "denied";
}
// Request permission from the user
try {
const permission = await Notification.requestPermission();
console.log("User responded with:", permission);
return permission;
} catch (error) {
console.error("Error requesting permission:", error);
return "error";
}
}
// Usage
requestNotificationPermission().then((result) => {
if (result === "granted") {
console.log("Ready to send notifications!");
}
});
The Notification Constructor
Once permission is granted, you create notifications using the new Notification(title, options) constructor. The first argument is the notification title (a required string), and the second is an optional configuration object that controls the appearance and behavior of the notification.
Example: Creating a Basic Notification
function showBasicNotification() {
if (Notification.permission !== "granted") {
console.log("No permission to show notifications.");
return;
}
const notification = new Notification("Hello World!", {
body: "This is your first browser notification.",
icon: "/images/notification-icon.png"
});
console.log("Notification created:", notification);
}
showBasicNotification();
Notification Options in Depth
The options object passed to the Notification constructor controls every aspect of how the notification looks and behaves. Here is a comprehensive breakdown of all available options.
Example: Full Notification Options
function showDetailedNotification() {
const options = {
// Text content
body: "You have 3 new messages from your team.",
// Visual elements
icon: "/images/app-icon-192.png", // Small icon (usually app logo)
badge: "/images/badge-icon-72.png", // Monochrome icon for status bar
image: "/images/preview-large.jpg", // Large image preview
// Grouping and replacement
tag: "messages-group", // Group ID -- replaces same-tag notifications
renotify: true, // Alert again even if replacing same tag
// Behavior
silent: false, // If true, no sound or vibration
requireInteraction: false, // If true, stays until user interacts
// Data payload
data: {
url: "/messages",
messageCount: 3,
timestamp: Date.now()
},
// Vibration pattern (mobile): vibrate, pause, vibrate
vibrate: [200, 100, 200],
// Text direction
dir: "ltr", // "ltr", "rtl", or "auto"
lang: "en-US", // Language tag
// Actions (only work with service worker notifications)
actions: [
{ action: "view", title: "View Messages", icon: "/images/view.png" },
{ action: "dismiss", title: "Dismiss", icon: "/images/dismiss.png" }
],
// Timestamp for when the event occurred
timestamp: Date.now() - 30000 // 30 seconds ago
};
const notification = new Notification("New Messages", options);
return notification;
}
tag property is particularly important for managing multiple notifications. When you create a new notification with the same tag as an existing one, the new notification replaces the old one rather than stacking a new entry. Set renotify: true if you want the replacement to still alert the user with sound and vibration. Without renotify, the replacement happens silently.Notification Events
Notifications fire four events that let you respond to user interaction. These events are essential for creating a responsive notification experience -- opening relevant pages when clicked, logging analytics on close, and handling errors gracefully.
Example: Handling All Notification Events
function showInteractiveNotification() {
const notification = new Notification("Task Reminder", {
body: "Your project deadline is in 1 hour.",
icon: "/images/task-icon.png",
tag: "task-reminder",
data: { taskId: 42, url: "/tasks/42" }
});
// Fired when the notification is displayed
notification.onshow = function(event) {
console.log("Notification shown:", event);
// Track notification display in analytics
trackEvent("notification_shown", { tag: "task-reminder" });
};
// Fired when the user clicks the notification
notification.onclick = function(event) {
event.preventDefault(); // Prevent default focus behavior
console.log("Notification clicked:", event);
// Open the relevant page
const url = event.target.data.url;
window.open(url, "_blank");
// Close the notification after clicking
notification.close();
};
// Fired when the notification is closed (by user or programmatically)
notification.onclose = function(event) {
console.log("Notification closed:", event);
trackEvent("notification_closed", { tag: "task-reminder" });
};
// Fired when something goes wrong
notification.onerror = function(event) {
console.error("Notification error:", event);
// Show a fallback in-app notification
showInAppFallback("Task Reminder", "Your deadline is in 1 hour.");
};
return notification;
}
function trackEvent(name, data) {
console.log("Analytics event:", name, data);
}
function showInAppFallback(title, message) {
// Display an in-app toast as fallback
console.log("Fallback notification:", title, "-", message);
}
Closing Notifications Programmatically
You can close a notification at any time using the close() method. This is useful for dismissing notifications after they have served their purpose, or for implementing auto-dismiss behavior with a timeout.
Example: Auto-Dismissing Notifications
function showTimedNotification(title, body, duration = 5000) {
const notification = new Notification(title, {
body: body,
icon: "/images/info-icon.png",
tag: "timed-" + Date.now()
});
// Auto-close after the specified duration
const timer = setTimeout(() => {
notification.close();
console.log("Notification auto-closed after", duration, "ms");
}, duration);
// Clear the timer if user manually closes or clicks
notification.onclose = function() {
clearTimeout(timer);
};
notification.onclick = function() {
clearTimeout(timer);
notification.close();
};
return notification;
}
// Show a notification that auto-dismisses after 8 seconds
showTimedNotification(
"Upload Complete",
"Your file has been uploaded successfully.",
8000
);
The Permissions API
While the Notification API has its own Notification.permission property, the broader Permissions API provides a unified way to query and monitor permissions for various browser features including notifications, geolocation, camera, microphone, and more. The Permissions API is accessed through navigator.permissions.
Example: Querying Notification Permission with the Permissions API
async function queryNotificationPermission() {
if (!navigator.permissions) {
console.log("Permissions API not supported.");
return null;
}
try {
const permissionStatus = await navigator.permissions.query({
name: "notifications"
});
console.log("Notification permission state:", permissionStatus.state);
// state can be: "granted", "denied", or "prompt"
return permissionStatus;
} catch (error) {
console.error("Error querying permission:", error);
return null;
}
}
queryNotificationPermission();
Monitoring Permission Changes
One of the most powerful features of the Permissions API is the ability to listen for permission changes in real time. The PermissionStatus object fires a change event whenever the user modifies the permission through browser settings.
Example: Watching for Permission Changes
async function watchNotificationPermission() {
if (!navigator.permissions) {
console.log("Permissions API not available.");
return;
}
try {
const status = await navigator.permissions.query({
name: "notifications"
});
console.log("Initial state:", status.state);
// Listen for changes
status.addEventListener("change", function() {
console.log("Permission changed to:", status.state);
switch (status.state) {
case "granted":
enableNotificationFeatures();
break;
case "denied":
disableNotificationFeatures();
showPermissionDeniedMessage();
break;
case "prompt":
showEnableNotificationsButton();
break;
}
});
} catch (error) {
console.error("Failed to watch permissions:", error);
}
}
function enableNotificationFeatures() {
console.log("Enabling notification UI elements...");
// Show notification preferences panel
// Enable notification toggle switches
}
function disableNotificationFeatures() {
console.log("Disabling notification UI elements...");
// Hide notification settings
// Show "notifications blocked" message
}
function showPermissionDeniedMessage() {
console.log("Showing instructions to re-enable in browser settings...");
}
function showEnableNotificationsButton() {
console.log("Showing Enable Notifications button...");
}
watchNotificationPermission();
Querying Multiple Permissions
The Permissions API supports querying various browser features. You can check multiple permissions to build comprehensive feature-detection logic for your application.
Example: Querying Multiple Permissions
async function checkAllPermissions() {
const permissionNames = [
"notifications",
"geolocation",
"camera",
"microphone"
];
const results = {};
for (const name of permissionNames) {
try {
const status = await navigator.permissions.query({ name });
results[name] = status.state;
} catch (error) {
results[name] = "unsupported";
}
}
console.log("Permission states:", results);
return results;
}
// Usage
checkAllPermissions().then((permissions) => {
if (permissions.notifications === "granted") {
console.log("Notifications are enabled.");
}
if (permissions.geolocation === "prompt") {
console.log("Geolocation permission not yet requested.");
}
});
Building a Complete Notification System
In a real application, you need a robust notification system that handles permission management, notification creation, event tracking, and fallback behavior in a single cohesive module. Here is a comprehensive notification manager class that encapsulates all of this functionality.
Example: Complete Notification Manager
class NotificationManager {
constructor(options = {}) {
this.defaultIcon = options.icon || "/images/default-icon.png";
this.defaultBadge = options.badge || "/images/default-badge.png";
this.defaultDuration = options.duration || 0; // 0 = no auto-close
this.activeNotifications = new Map();
this.eventListeners = new Map();
this.isSupported = "Notification" in window;
}
// Check if notifications are supported
get supported() {
return this.isSupported;
}
// Get current permission state
get permission() {
if (!this.isSupported) return "unsupported";
return Notification.permission;
}
// Request permission from user
async requestPermission() {
if (!this.isSupported) {
return "unsupported";
}
if (Notification.permission === "granted") {
return "granted";
}
if (Notification.permission === "denied") {
return "denied";
}
try {
const result = await Notification.requestPermission();
this.emit("permissionchange", result);
return result;
} catch (error) {
console.error("Permission request failed:", error);
return "error";
}
}
// Show a notification
show(title, options = {}) {
if (!this.isSupported || Notification.permission !== "granted") {
this.showFallback(title, options);
return null;
}
const notificationOptions = {
body: options.body || "",
icon: options.icon || this.defaultIcon,
badge: options.badge || this.defaultBadge,
tag: options.tag || "default-" + Date.now(),
data: options.data || {},
silent: options.silent || false,
renotify: options.renotify || false,
requireInteraction: options.requireInteraction || false,
...options
};
try {
const notification = new Notification(title, notificationOptions);
// Store reference
const id = notificationOptions.tag;
this.activeNotifications.set(id, notification);
// Attach event handlers
notification.onclick = (event) => {
event.preventDefault();
this.emit("click", { notification, event, data: notificationOptions.data });
if (options.onClick) options.onClick(event, notificationOptions.data);
notification.close();
};
notification.onclose = () => {
this.activeNotifications.delete(id);
this.emit("close", { id, data: notificationOptions.data });
if (options.onClose) options.onClose(notificationOptions.data);
};
notification.onerror = (event) => {
this.emit("error", { event, id });
this.showFallback(title, options);
};
notification.onshow = () => {
this.emit("show", { id, title });
};
// Auto-close if duration is set
const duration = options.duration || this.defaultDuration;
if (duration > 0) {
setTimeout(() => {
if (this.activeNotifications.has(id)) {
notification.close();
}
}, duration);
}
return notification;
} catch (error) {
console.error("Failed to create notification:", error);
this.showFallback(title, options);
return null;
}
}
// Close a specific notification by tag
close(tag) {
const notification = this.activeNotifications.get(tag);
if (notification) {
notification.close();
this.activeNotifications.delete(tag);
}
}
// Close all active notifications
closeAll() {
this.activeNotifications.forEach((notification) => {
notification.close();
});
this.activeNotifications.clear();
}
// Get count of active notifications
get activeCount() {
return this.activeNotifications.size;
}
// In-app fallback when notifications are not available
showFallback(title, options = {}) {
const container = document.getElementById("notification-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = "in-app-notification";
toast.innerHTML = "<strong>" + title + "</strong>"
+ (options.body ? "<p>" + options.body + "</p>" : "");
container.appendChild(toast);
setTimeout(() => {
toast.classList.add("fade-out");
setTimeout(() => toast.remove(), 300);
}, options.duration || 5000);
this.emit("fallback", { title, options });
}
// Simple event emitter
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}
off(event, callback) {
if (!this.eventListeners.has(event)) return;
const listeners = this.eventListeners.get(event);
const index = listeners.indexOf(callback);
if (index > -1) listeners.splice(index, 1);
}
emit(event, data) {
if (!this.eventListeners.has(event)) return;
this.eventListeners.get(event).forEach((cb) => cb(data));
}
}
// Usage
const notifier = new NotificationManager({
icon: "/images/app-icon.png",
duration: 10000 // Auto-close after 10 seconds
});
// Listen for events
notifier.on("click", ({ data }) => {
console.log("User clicked notification, data:", data);
if (data.url) window.open(data.url, "_blank");
});
notifier.on("permissionchange", (state) => {
console.log("Permission changed:", state);
});
Using the Notification Manager
With the notification manager in place, sending notifications becomes straightforward and consistent across your entire application.
Example: Practical Usage of the Notification Manager
// Initialize the manager
const notifier = new NotificationManager({
icon: "/images/app-icon.png"
});
// Request permission on user interaction
document.getElementById("enable-btn").addEventListener("click", async () => {
const permission = await notifier.requestPermission();
if (permission === "granted") {
notifier.show("Notifications Enabled!", {
body: "You will now receive important updates.",
tag: "welcome",
duration: 5000
});
} else if (permission === "denied") {
alert("Notifications blocked. Enable them in browser settings.");
}
});
// Send different types of notifications
function notifyNewMessage(sender, message, chatUrl) {
notifier.show("New Message from " + sender, {
body: message,
tag: "chat-" + sender.toLowerCase().replace(/\s/g, "-"),
renotify: true,
data: { url: chatUrl, type: "message" },
onClick: (event, data) => {
window.focus();
window.location.href = data.url;
}
});
}
function notifyTaskDeadline(taskName, timeLeft) {
notifier.show("Deadline Approaching", {
body: taskName + " is due in " + timeLeft + ".",
tag: "deadline-" + taskName,
requireInteraction: true,
data: { url: "/tasks", type: "deadline" }
});
}
function notifyUploadComplete(fileName) {
notifier.show("Upload Complete", {
body: fileName + " has been uploaded successfully.",
tag: "upload-complete",
silent: true,
duration: 6000
});
}
// Simulate real-time events
notifyNewMessage("Alice", "Hey, can you review the PR?", "/chat/alice");
notifyTaskDeadline("Project Report", "2 hours");
notifyUploadComplete("presentation.pdf");
Introduction to Service Worker Notifications
The notifications we have created so far use the basic Notification API, which requires the page to be open. For true push notifications that work even when your site is closed, you need Service Workers. Service worker notifications use self.registration.showNotification() and support additional features like action buttons.
Example: Service Worker Notification Basics
// In your main JavaScript file: Register the service worker
async function registerServiceWorker() {
if (!("serviceWorker" in navigator)) {
console.log("Service workers not supported.");
return null;
}
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);
return null;
}
}
// Show a notification through the service worker
async function showServiceWorkerNotification(title, options) {
const registration = await navigator.serviceWorker.ready;
await registration.showNotification(title, {
body: options.body || "",
icon: options.icon || "/images/icon-192.png",
badge: options.badge || "/images/badge-72.png",
tag: options.tag || "sw-notification",
data: options.data || {},
actions: options.actions || [],
vibrate: options.vibrate || [200, 100, 200]
});
}
// Usage
registerServiceWorker().then(() => {
showServiceWorkerNotification("Background Update", {
body: "Your data has been synced.",
tag: "sync-complete",
actions: [
{ action: "view", title: "View Changes" },
{ action: "dismiss", title: "Dismiss" }
],
data: { url: "/dashboard" }
});
});
Example: Handling Notification Events in the Service Worker (sw.js)
// sw.js -- Service Worker file
// Handle notification click
self.addEventListener("notificationclick", function(event) {
const notification = event.notification;
const action = event.action;
const data = notification.data;
notification.close();
if (action === "view") {
// Open the relevant page
event.waitUntil(
clients.openWindow(data.url || "/")
);
} else if (action === "dismiss") {
// Just close the notification (already done above)
console.log("Notification dismissed by user.");
} else {
// Default click (no specific action button)
event.waitUntil(
clients.matchAll({ type: "window" }).then(function(clientList) {
// Focus existing window if open
for (const client of clientList) {
if (client.url.includes(data.url) && "focus" in client) {
return client.focus();
}
}
// Otherwise open a new window
return clients.openWindow(data.url || "/");
})
);
}
});
// Handle notification close (dismissed without clicking)
self.addEventListener("notificationclose", function(event) {
const data = event.notification.data;
console.log("Notification closed without interaction:", data);
// Track dismissal analytics
});
showNotification() to display them. This architecture enables real-time notifications even when the user has closed your website. The combination of Push API and Service Worker Notifications is what powers the notification systems of progressive web apps like Twitter, Slack, and other modern web applications.Real-World Example: Chat Application Notifications
Here is a practical implementation of notifications for a chat application. It handles different message types, groups conversations, and intelligently manages notification stacking.
Example: Chat Application Notification System
class ChatNotificationSystem {
constructor() {
this.notifier = new NotificationManager({
icon: "/images/chat-icon.png"
});
this.unreadCounts = new Map();
this.isWindowFocused = true;
// Track window focus state
window.addEventListener("focus", () => {
this.isWindowFocused = true;
this.clearAllNotifications();
});
window.addEventListener("blur", () => {
this.isWindowFocused = false;
});
}
// Handle incoming message
onMessageReceived(message) {
// Do not notify if window is focused and user is on the chat page
if (this.isWindowFocused && this.isUserOnChatPage(message.senderId)) {
return;
}
// Update unread count for this conversation
const currentCount = this.unreadCounts.get(message.senderId) || 0;
this.unreadCounts.set(message.senderId, currentCount + 1);
const count = currentCount + 1;
const senderName = message.senderName;
// Build notification body based on message count
let body;
if (count === 1) {
body = message.text;
} else {
body = count + " new messages";
}
// Show notification grouped by sender
this.notifier.show(senderName, {
body: body,
tag: "chat-" + message.senderId, // Groups by sender
renotify: true, // Alert for each new message
icon: message.senderAvatar || "/images/default-avatar.png",
data: {
url: "/chat/" + message.senderId,
senderId: message.senderId,
type: "chat-message"
},
onClick: (event, data) => {
window.focus();
this.navigateToChat(data.senderId);
this.unreadCounts.delete(data.senderId);
}
});
}
// Handle typing indicator
onTypingIndicator(senderId, senderName) {
// Do not create a notification for typing, but could update existing one
console.log(senderName + " is typing...");
}
// Check if user is currently viewing a specific chat
isUserOnChatPage(senderId) {
return window.location.pathname === "/chat/" + senderId;
}
// Navigate to a chat conversation
navigateToChat(senderId) {
window.location.href = "/chat/" + senderId;
}
// Clear all notifications when window gains focus
clearAllNotifications() {
this.notifier.closeAll();
}
}
// Initialize
const chatNotifications = new ChatNotificationSystem();
// Simulate incoming messages
chatNotifications.onMessageReceived({
senderId: "user-101",
senderName: "Sarah",
senderAvatar: "/images/avatars/sarah.jpg",
text: "Can you check the deployment logs?"
});
Real-World Example: Task Reminder System
This example demonstrates a task reminder system that schedules notifications at specified intervals before a deadline and handles snooze functionality.
Example: Task Reminder Notification System
class TaskReminderSystem {
constructor() {
this.notifier = new NotificationManager({
icon: "/images/task-icon.png"
});
this.scheduledReminders = new Map();
}
// Schedule a reminder for a task
scheduleReminder(task) {
const now = Date.now();
const deadline = new Date(task.deadline).getTime();
const timeUntilDeadline = deadline - now;
if (timeUntilDeadline <= 0) {
this.showOverdueNotification(task);
return;
}
// Define reminder intervals
const reminders = [];
// 1 hour before
if (timeUntilDeadline > 3600000) {
reminders.push({
delay: timeUntilDeadline - 3600000,
label: "1 hour"
});
}
// 15 minutes before
if (timeUntilDeadline > 900000) {
reminders.push({
delay: timeUntilDeadline - 900000,
label: "15 minutes"
});
}
// At deadline
reminders.push({
delay: timeUntilDeadline,
label: "now"
});
// Schedule each reminder
const timers = reminders.map((reminder) => {
return setTimeout(() => {
this.showReminderNotification(task, reminder.label);
}, reminder.delay);
});
this.scheduledReminders.set(task.id, timers);
console.log("Scheduled", timers.length, "reminders for:", task.name);
}
// Show the reminder notification
showReminderNotification(task, timeLabel) {
const isOverdue = timeLabel === "now";
const title = isOverdue ? "Deadline Reached!" : "Deadline in " + timeLabel;
this.notifier.show(title, {
body: task.name + (isOverdue
? " -- This task is due now!"
: " -- Due in " + timeLabel + "."),
tag: "task-" + task.id,
renotify: true,
requireInteraction: isOverdue,
data: {
taskId: task.id,
url: "/tasks/" + task.id,
type: "task-reminder"
},
onClick: (event, data) => {
window.focus();
window.location.href = data.url;
}
});
}
// Show overdue notification
showOverdueNotification(task) {
this.notifier.show("Overdue Task!", {
body: task.name + " was due " + this.getTimeAgo(task.deadline) + ".",
tag: "overdue-" + task.id,
requireInteraction: true,
data: { taskId: task.id, url: "/tasks/" + task.id }
});
}
// Cancel reminders for a task
cancelReminder(taskId) {
const timers = this.scheduledReminders.get(taskId);
if (timers) {
timers.forEach((timer) => clearTimeout(timer));
this.scheduledReminders.delete(taskId);
console.log("Cancelled reminders for task:", taskId);
}
}
// Helper: get human-readable time ago
getTimeAgo(dateString) {
const diff = Date.now() - new Date(dateString).getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 60) return minutes + " minutes ago";
const hours = Math.floor(minutes / 60);
if (hours < 24) return hours + " hours ago";
return Math.floor(hours / 24) + " days ago";
}
}
// Usage
const reminders = new TaskReminderSystem();
reminders.scheduleReminder({
id: "task-001",
name: "Submit quarterly report",
deadline: new Date(Date.now() + 3700000).toISOString() // ~1 hour from now
});
Notification Best Practices
Building a good notification experience requires careful attention to user experience, timing, and content. Follow these best practices to ensure your notifications are helpful rather than annoying.
- Ask at the right time -- Request permission only after a meaningful user action (clicking "Enable Notifications"), never on page load. Explain why notifications will be valuable before asking.
- Keep content concise -- Notification titles should be under 50 characters. Body text should be under 120 characters. Users scan notifications quickly, so every word must count.
- Use tags for grouping -- Assign meaningful
tagvalues to prevent notification flooding. Group related notifications (such as all messages from the same chat) under the same tag. - Provide a fallback -- Always implement an in-app notification fallback for when the browser does not support notifications or the user has denied permission.
- Respect user context -- Do not send notifications when the user is actively using your application. Check window focus state before triggering notifications.
- Allow granular control -- Provide settings that let users choose which types of notifications they want (messages, reminders, updates) rather than forcing all-or-nothing.
- Limit frequency -- Implement rate limiting to prevent spamming users with too many notifications in a short period. Queue notifications and batch them if necessary.
- Include actionable data -- Always include a
datapayload with a URL or identifier so that clicking the notification takes the user to the relevant content. - Handle all events -- Always attach
onclick,onclose, andonerrorhandlers. Do not leave notification interactions unhandled. - Test across platforms -- Notification appearance varies significantly across operating systems (Windows, macOS, Linux, Android). Test on all target platforms to ensure a good experience.
localhost is treated as a secure context, but once deployed, your site must use HTTPS. Additionally, some browsers throttle notifications from background tabs and may silently drop them if too many are sent in a short period.Practice Exercise
Build a complete notification system for a project management application with the following requirements: (1) Create a NotificationManager class that checks for support, requests permission with proper timing (only after user clicks an "Enable" button), and stores the permission state. (2) Implement three notification types: "New Task Assigned" (with the task name, assignee, and a link to the task), "Comment Added" (grouped by task using tags so multiple comments on the same task replace each other), and "Deadline Reminder" (scheduled to fire 30 minutes and 5 minutes before a task deadline, with requireInteraction: true). (3) Add a fallback in-app toast notification system that displays when browser notifications are blocked or unsupported. (4) Use the Permissions API to monitor permission changes in real time and update the UI accordingly (show an "Enable" button when permission is "prompt", show a settings message when "denied", and show notification preferences when "granted"). (5) Implement rate limiting so that no more than 5 notifications are shown within any 30-second window. (6) Add window focus tracking so that notifications are suppressed when the user is actively viewing the relevant page. Test your system by simulating a series of rapid events and verify that grouping, rate limiting, and focus tracking all work correctly.