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.

JavaScriptintermediate
15 min read

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

javascriptjavascript
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 StateMeaning
defaultUser has not been asked yet or dismissed the prompt
grantedUser allowed notifications
deniedUser blocked notifications, cannot re-prompt

Creating Notifications

javascriptjavascript
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

OptionTypeDescription
bodystringSecondary text below the title
iconstringURL of image displayed beside the notification
badgestringSmall icon for mobile notification bar
imagestringLarge image displayed in the notification body
tagstringID for grouping; new notifications with the same tag replace old ones
dataanyCustom data attached to the notification object
requireInteractionbooleanKeep notification visible until user interacts
silentbooleanSuppress vibration and sound
dirstringText direction: auto, ltr, or rtl
renotifybooleanRe-alert user when replacing a tagged notification

Handling Notification Events

javascriptjavascript
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

javascriptjavascript
// 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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

Rune AI

Key Insights

  • Permission is required before showing: Always check Notification.permission and call requestPermission() before creating notifications, with graceful fallbacks for denied states
  • Tag property groups and replaces: Notifications sharing the same tag replace each other, allowing you to update counts or statuses without stacking
  • Event handling enables interaction: Listen for click, close, show, and error events 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
RunePowered by Rune AI

Frequently Asked Questions

Do notifications work when the browser tab is in the background?

Yes. Desktop notifications display in the operating system notification area regardless of whether the tab is active. That is the primary benefit of the Notifications API compared to in-app toast messages. The `click` event handler can then call `window.focus()` to bring the tab back.

What is the difference between Notifications API and Push API?

The Notifications API shows a desktop notification from the current page. The Push API, combined with Service Workers, receives server-sent messages even when the tab is closed. They often work together: the Push API triggers the event, and the Notifications API (via `ServiceWorkerRegistration.showNotification`) displays it.

Can I include action buttons in notifications?

ction buttons require a Service Worker context. Use `registration.showNotification(title, { actions: [{ action: "reply", title: "Reply" }] })` inside a Service Worker. The plain `new Notification()` constructor does not support the `actions` option in most browsers.

How do I handle users who denied notifications?

Once denied, the browser will not show the permission prompt again. Provide an in-app fallback (toast messages or banner alerts) and include instructions for the user to re-enable notifications through browser settings. Use `Notification.permission === "denied"` to detect this state.

Are web notifications the same across all operating systems?

No. Appearance varies by OS. Windows uses the Action Center, macOS uses Notification Center, and Linux uses the desktop environment notification daemon. The `icon`, `badge`, and `image` options may render differently or be ignored on some platforms.

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.