JavaScript Notifications API: Complete Tutorial
A complete tutorial on the JavaScript Notifications API. Covers showing desktop notifications, setting title/body/icon/badge, handling click and close events, notification actions, tag-based replacement, silent notifications, notification queues, and building a full notification manager.
The Notifications API lets web applications send desktop notifications outside the browser window. These notifications appear in the operating system notification area and persist even when the user switches to another tab or application.
For managing notification permissions, see Requesting Desktop Notification Permissions in JS.
Checking Support and Permission
function checkNotificationSupport() {
if (!("Notification" in window)) {
console.warn("Notifications not supported");
return false;
}
return true;
}
function getPermissionStatus() {
if (!checkNotificationSupport()) return "unsupported";
return Notification.permission; // "default", "granted", or "denied"
}
async function ensurePermission() {
if (!checkNotificationSupport()) return false;
if (Notification.permission === "granted") return true;
if (Notification.permission === "denied") return false;
const result = await Notification.requestPermission();
return result === "granted";
}| Permission State | Meaning |
|---|---|
default | User has not been asked yet or dismissed the prompt |
granted | User allowed notifications |
denied | User blocked notifications, cannot re-prompt |
Creating Notifications
async function showNotification(title, options = {}) {
const allowed = await ensurePermission();
if (!allowed) return null;
const notification = new Notification(title, {
body: options.body || "",
icon: options.icon || "/icons/default-96.png",
badge: options.badge || "/icons/badge-72.png",
image: options.image,
tag: options.tag,
data: options.data,
requireInteraction: options.requireInteraction || false,
silent: options.silent || false,
dir: options.dir || "auto",
lang: options.lang || "en",
renotify: options.renotify || false,
});
return notification;
}
// Simple notification
showNotification("New Message", {
body: "You have 3 unread messages",
icon: "/icons/message.png",
});
// Rich notification
showNotification("Order Shipped", {
body: "Your order #12345 is on its way!",
icon: "/icons/shipping.png",
image: "/images/package-tracking.png",
tag: "order-12345",
data: { orderId: 12345, url: "/orders/12345" },
requireInteraction: true,
});Notification Options Reference
| Option | Type | Description |
|---|---|---|
body | string | Secondary text below the title |
icon | string | URL of image displayed beside the notification |
badge | string | Small icon for mobile notification bar |
image | string | Large image displayed in the notification body |
tag | string | ID for grouping; new notifications with the same tag replace old ones |
data | any | Custom data attached to the notification object |
requireInteraction | boolean | Keep notification visible until user interacts |
silent | boolean | Suppress vibration and sound |
dir | string | Text direction: auto, ltr, or rtl |
renotify | boolean | Re-alert user when replacing a tagged notification |
Handling Notification Events
function createInteractiveNotification(title, options = {}) {
const notification = new Notification(title, options);
notification.addEventListener("click", (event) => {
event.preventDefault();
// Focus the window
window.focus();
// Navigate if data contains a URL
if (notification.data && notification.data.url) {
window.location.href = notification.data.url;
}
notification.close();
});
notification.addEventListener("close", () => {
console.log("Notification closed:", title);
});
notification.addEventListener("error", (event) => {
console.error("Notification error:", event);
});
notification.addEventListener("show", () => {
console.log("Notification shown:", title);
});
return notification;
}
// Usage
createInteractiveNotification("Invoice Ready", {
body: "Click to view invoice #789",
data: { url: "/invoices/789" },
tag: "invoice-789",
});Tag-Based Replacement
// First notification
new Notification("2 new emails", {
body: "From: Alice, Bob",
tag: "email-count",
});
// Seconds later, this replaces the first notification
new Notification("5 new emails", {
body: "From: Alice, Bob, Charlie, Dana, Eve",
tag: "email-count",
renotify: true, // Re-alert the user
});When two notifications share the same tag, the new one replaces the old. Without renotify: true, the replacement happens silently.
Auto-Closing Notifications
function showTimedNotification(title, options = {}, duration = 5000) {
const notification = new Notification(title, options);
const timer = setTimeout(() => {
notification.close();
}, duration);
notification.addEventListener("click", () => {
clearTimeout(timer);
notification.close();
});
notification.addEventListener("close", () => {
clearTimeout(timer);
});
return notification;
}
// Disappears after 8 seconds
showTimedNotification(
"Quick Update",
{ body: "Sync complete" },
8000
);Notification Queue
class NotificationQueue {
constructor(maxConcurrent = 3, spacing = 300) {
this.maxConcurrent = maxConcurrent;
this.spacing = spacing;
this.queue = [];
this.active = new Set();
this.processing = false;
}
add(title, options = {}, duration = 0) {
return new Promise((resolve) => {
this.queue.push({ title, options, duration, resolve });
this.process();
});
}
async process() {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0) {
if (this.active.size >= this.maxConcurrent) {
await new Promise((resolve) => setTimeout(resolve, 100));
continue;
}
const item = this.queue.shift();
this.showItem(item);
await new Promise((resolve) => setTimeout(resolve, this.spacing));
}
this.processing = false;
}
showItem(item) {
const notification = new Notification(item.title, item.options);
this.active.add(notification);
const cleanup = () => {
this.active.delete(notification);
item.resolve(notification);
};
notification.addEventListener("close", cleanup);
if (item.duration > 0) {
setTimeout(() => {
notification.close();
}, item.duration);
}
}
clearQueue() {
this.queue = [];
}
closeAll() {
this.active.forEach((n) => n.close());
this.active.clear();
this.clearQueue();
}
}
// Usage
const queue = new NotificationQueue(2, 500);
queue.add("Alert 1", { body: "First notification" }, 5000);
queue.add("Alert 2", { body: "Second notification" }, 5000);
queue.add("Alert 3", { body: "Third (queued)" }, 5000);Full Notification Manager
class NotificationManager {
constructor() {
this.supported = "Notification" in window;
this.queue = new NotificationQueue(3);
this.history = [];
this.maxHistory = 50;
this.listeners = new Map();
}
async requestPermission() {
if (!this.supported) return false;
if (Notification.permission === "granted") return true;
if (Notification.permission === "denied") return false;
const result = await Notification.requestPermission();
return result === "granted";
}
isPermitted() {
return this.supported && Notification.permission === "granted";
}
async send(title, options = {}) {
if (!this.isPermitted()) {
const granted = await this.requestPermission();
if (!granted) {
this.fallbackNotification(title, options);
return null;
}
}
const notification = await this.queue.add(
title,
options,
options.duration || 0
);
this.history.push({
title,
options,
timestamp: Date.now(),
});
if (this.history.length > this.maxHistory) {
this.history.shift();
}
this.emit("sent", { title, options, notification });
return notification;
}
fallbackNotification(title, options) {
// In-app fallback when notifications are blocked
const container = document.getElementById("notification-fallback");
if (!container) return;
const el = document.createElement("div");
el.className = "in-app-notification";
el.innerHTML = `
<strong>${title}</strong>
<p>${options.body || ""}</p>
<button onclick="this.parentElement.remove()">Dismiss</button>
`;
container.prepend(el);
setTimeout(() => {
if (el.parentElement) el.remove();
}, options.duration || 8000);
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(callback);
}
off(event, callback) {
const cbs = this.listeners.get(event);
if (cbs) cbs.delete(callback);
}
emit(event, data) {
const cbs = this.listeners.get(event);
if (cbs) cbs.forEach((cb) => cb(data));
}
getHistory() {
return [...this.history];
}
clearHistory() {
this.history = [];
}
closeAll() {
this.queue.closeAll();
}
}
// Usage
const notifier = new NotificationManager();
notifier.on("sent", ({ title }) => {
console.log("Notification sent:", title);
});
notifier.send("Welcome Back", {
body: "You have 3 pending tasks",
icon: "/icons/tasks.png",
data: { url: "/dashboard" },
duration: 6000,
});Rune AI
Key Insights
- Permission is required before showing: Always check
Notification.permissionand callrequestPermission()before creating notifications, with graceful fallbacks for denied states - Tag property groups and replaces: Notifications sharing the same
tagreplace each other, allowing you to update counts or statuses without stacking - Event handling enables interaction: Listen for
click,close,show, anderrorevents to navigate users, track engagement, and handle failures - Queue system prevents notification spam: Limit concurrent notifications and add spacing delays to avoid overwhelming users with rapid-fire alerts
- In-app fallback is essential: Always provide toast or banner notifications for when desktop notifications are blocked or unsupported
Frequently Asked Questions
Do notifications work when the browser tab is in the background?
What is the difference between Notifications API and Push API?
Can I include action buttons in notifications?
How do I handle users who denied notifications?
Are web notifications the same across all operating systems?
Conclusion
The Notifications API provides desktop alerts through a straightforward constructor with rich options for icons, tags, data payloads, and event handling. Use tag-based replacement for updating counts, queues for rate limiting, and always provide in-app fallbacks. For permission handling patterns and UX best practices, see Requesting Desktop Notification Permissions in JS. For storing notification preferences, see JS localStorage API Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.