Requesting Desktop Notification Permissions in JS
A complete tutorial on requesting desktop notification permissions in JavaScript. Covers Notification.requestPermission(), Permissions API query, permission states, best practices for timing prompts, progressive permission flows, detecting denied state, and building a permission manager with localStorage persistence.
Requesting notification permission at the right time and handling every state correctly determines whether users allow or permanently block your notifications. This guide covers the permission lifecycle, best practices for prompting, and building a permission manager.
For the full Notifications API reference, see JavaScript Notifications API: Complete Tutorial.
Permission States
The browser tracks notification permission as one of three states:
| State | Value | Meaning |
|---|---|---|
| Default | "default" | User has not decided; you can request permission |
| Granted | "granted" | User allowed notifications |
| Denied | "denied" | User blocked notifications; browser will not re-prompt |
function getNotificationState() {
if (!("Notification" in window)) {
return "unsupported";
}
return Notification.permission;
}
// Check without prompting
const state = getNotificationState();
console.log("Current permission:", state);Requesting Permission
// Modern async/await approach
async function requestNotificationPermission() {
if (!("Notification" in window)) {
return { granted: false, reason: "unsupported" };
}
if (Notification.permission === "granted") {
return { granted: true, reason: "already-granted" };
}
if (Notification.permission === "denied") {
return { granted: false, reason: "previously-denied" };
}
try {
const result = await Notification.requestPermission();
return {
granted: result === "granted",
reason: result, // "granted", "denied", or "default" (dismissed)
};
} catch (error) {
// Fallback for older callback-based API
return new Promise((resolve) => {
Notification.requestPermission((result) => {
resolve({
granted: result === "granted",
reason: result,
});
});
});
}
}
// Usage
const permission = await requestNotificationPermission();
if (permission.granted) {
new Notification("Notifications enabled!");
}Using the Permissions API
The Permissions API provides a way to query and watch permission state changes without triggering a prompt:
async function queryNotificationPermission() {
if (!("permissions" in navigator)) {
// Fall back to Notification.permission
return { state: Notification.permission };
}
try {
const status = await navigator.permissions.query({
name: "notifications",
});
return {
state: status.state, // "granted", "denied", or "prompt"
status,
};
} catch (error) {
return { state: Notification.permission };
}
}
// Watch for permission changes (e.g., user changes in browser settings)
async function watchPermissionChanges(callback) {
if (!("permissions" in navigator)) return null;
try {
const status = await navigator.permissions.query({
name: "notifications",
});
status.addEventListener("change", () => {
callback(status.state);
});
return status;
} catch (error) {
return null;
}
}
// Usage
watchPermissionChanges((newState) => {
console.log("Permission changed to:", newState);
if (newState === "denied") {
showInAppFallbackBanner();
}
});Progressive Permission Flow
Never prompt on page load. Use a two-step approach where the user first clicks an in-app button, then sees the browser prompt:
class ProgressivePermission {
constructor() {
this.prePromptShown = false;
this.container = null;
}
shouldShowPrePrompt() {
if (Notification.permission !== "default") return false;
if (localStorage.getItem("notification-prompt-dismissed")) return false;
return true;
}
showPrePrompt(container) {
if (!this.shouldShowPrePrompt()) return;
this.container = container;
container.innerHTML = `
<div class="permission-prompt" role="dialog" aria-label="Enable notifications">
<div class="prompt-content">
<h3>Stay Updated</h3>
<p>Get notified when new tutorials are published.</p>
<div class="prompt-actions">
<button id="enable-notifications" class="btn-primary">
Enable Notifications
</button>
<button id="dismiss-notifications" class="btn-secondary">
Not Now
</button>
</div>
</div>
</div>
`;
this.prePromptShown = true;
document.getElementById("enable-notifications")
.addEventListener("click", () => this.handleEnable());
document.getElementById("dismiss-notifications")
.addEventListener("click", () => this.handleDismiss());
}
async handleEnable() {
const result = await Notification.requestPermission();
if (result === "granted") {
this.showSuccess();
new Notification("Notifications enabled!", {
body: "You will receive updates for new content.",
icon: "/icons/check-96.png",
});
} else if (result === "denied") {
this.showDenied();
} else {
// Dismissed (still "default")
this.removePrompt();
}
}
handleDismiss() {
localStorage.setItem("notification-prompt-dismissed", Date.now().toString());
this.removePrompt();
}
showSuccess() {
if (this.container) {
this.container.innerHTML = `
<div class="permission-success" role="status">
Notifications enabled! You are all set.
</div>
`;
setTimeout(() => this.removePrompt(), 3000);
}
}
showDenied() {
if (this.container) {
this.container.innerHTML = `
<div class="permission-denied" role="status">
<p>Notifications are blocked. To enable them:</p>
<ol>
<li>Click the lock icon in the address bar</li>
<li>Find "Notifications" in site settings</li>
<li>Change from "Block" to "Allow"</li>
</ol>
</div>
`;
}
}
removePrompt() {
if (this.container) {
this.container.innerHTML = "";
}
}
}
// Usage: show after user engagement (e.g., after reading 2 articles)
const pageViews = parseInt(sessionStorage.getItem("page-views") || "0", 10) + 1;
sessionStorage.setItem("page-views", pageViews.toString());
if (pageViews >= 3) {
const prompt = new ProgressivePermission();
prompt.showPrePrompt(document.getElementById("notification-area"));
}Permission Manager with Persistence
class PermissionManager {
constructor(storageKey = "notification-prefs") {
this.storageKey = storageKey;
this.prefs = this.loadPrefs();
this.watchers = new Set();
this.startWatching();
}
loadPrefs() {
try {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : {
prompted: false,
promptedAt: null,
dismissCount: 0,
lastDismissed: null,
enabledAt: null,
deniedAt: null,
};
} catch {
return {
prompted: false,
promptedAt: null,
dismissCount: 0,
lastDismissed: null,
enabledAt: null,
deniedAt: null,
};
}
}
savePrefs() {
localStorage.setItem(this.storageKey, JSON.stringify(this.prefs));
}
getCurrentState() {
if (!("Notification" in window)) return "unsupported";
return Notification.permission;
}
canPrompt() {
const state = this.getCurrentState();
if (state !== "default") return false;
// Respect dismiss cooldown: 7 days after each dismiss
if (this.prefs.lastDismissed) {
const cooldown = 7 * 24 * 60 * 60 * 1000;
const elapsed = Date.now() - this.prefs.lastDismissed;
if (elapsed < cooldown) return false;
}
// Give up after 3 dismissals
if (this.prefs.dismissCount >= 3) return false;
return true;
}
async prompt() {
if (!this.canPrompt()) {
return { success: false, reason: "cannot-prompt" };
}
this.prefs.prompted = true;
this.prefs.promptedAt = Date.now();
const result = await Notification.requestPermission();
if (result === "granted") {
this.prefs.enabledAt = Date.now();
this.savePrefs();
this.notifyWatchers("granted");
return { success: true, reason: "granted" };
}
if (result === "denied") {
this.prefs.deniedAt = Date.now();
this.savePrefs();
this.notifyWatchers("denied");
return { success: false, reason: "denied" };
}
// Dismissed
this.prefs.dismissCount += 1;
this.prefs.lastDismissed = Date.now();
this.savePrefs();
this.notifyWatchers("dismissed");
return { success: false, reason: "dismissed" };
}
recordDismiss() {
this.prefs.dismissCount += 1;
this.prefs.lastDismissed = Date.now();
this.savePrefs();
}
async startWatching() {
if (!("permissions" in navigator)) return;
try {
const status = await navigator.permissions.query({ name: "notifications" });
status.addEventListener("change", () => {
this.notifyWatchers(status.state);
});
} catch {
// Permissions API not available for notifications
}
}
onStateChange(callback) {
this.watchers.add(callback);
return () => this.watchers.delete(callback);
}
notifyWatchers(state) {
for (const cb of this.watchers) {
cb(state);
}
}
getStats() {
return {
currentState: this.getCurrentState(),
canPrompt: this.canPrompt(),
dismissCount: this.prefs.dismissCount,
enabledAt: this.prefs.enabledAt,
deniedAt: this.prefs.deniedAt,
};
}
reset() {
localStorage.removeItem(this.storageKey);
this.prefs = this.loadPrefs();
}
}
// Usage
const permManager = new PermissionManager();
permManager.onStateChange((state) => {
console.log("Notification state changed:", state);
});
if (permManager.canPrompt()) {
// Show your custom pre-prompt UI
showPrePromptUI(() => permManager.prompt());
}Timing Strategies
| Strategy | When | Why |
|---|---|---|
| After user action | User clicks "Get notified" button | High intent = high acceptance |
| After engagement | 3+ page views or 30+ seconds on page | Demonstrates value before asking |
| After value delivery | User completes a tutorial or saves content | Positive moment = higher acceptance |
| Re-prompt with cooldown | 7+ days after a dismiss | Respects the "Not now" decision |
| Never on first visit | Skip first-time visitors entirely | No context for the request yet |
Detecting Denied State
function handleDeniedPermission() {
if (Notification.permission !== "denied") return;
// Detect browser for specific instructions
const isChrome = /Chrome/.test(navigator.userAgent) && !/Edg/.test(navigator.userAgent);
const isFirefox = /Firefox/.test(navigator.userAgent);
const isEdge = /Edg/.test(navigator.userAgent);
const isSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
let instructions = "";
if (isChrome || isEdge) {
instructions = "Click the lock/tune icon in the address bar, find Notifications, and set to Allow.";
} else if (isFirefox) {
instructions = "Click the shield icon in the address bar, then Permissions, and clear the notification block.";
} else if (isSafari) {
instructions = "Go to Safari > Settings > Websites > Notifications and allow this site.";
} else {
instructions = "Check your browser settings to allow notifications for this site.";
}
return instructions;
}Rune AI
Key Insights
- Three permission states require different handling:
defaultallows prompting,grantedenables notifications immediately, anddeniedrequires manual user action in browser settings - Progressive prompting improves acceptance rates: Show an in-app pre-prompt first so users understand the value, then trigger the browser prompt only after explicit user action
- Track dismissals with cooldowns: Store dismiss counts and timestamps in localStorage, implementing 7-day cooldowns and giving up after 3 dismissals
- Permissions API enables state watching: Use
navigator.permissions.query()to read state without prompting and listen forchangeevents when users modify settings externally - Always provide denied-state recovery instructions: Detect the browser type and show specific step-by-step instructions for re-enabling notifications through settings
Frequently Asked Questions
Why does the browser not show the permission prompt again after the user dismisses it?
Can I detect if the user dismissed the prompt without choosing?
How do I re-enable notifications after the user denied them?
Does the Permissions API replace Notification.requestPermission?
Should I use Service Worker notifications instead of the Notification constructor?
Conclusion
Requesting notification permissions effectively requires progressive prompting, respect for user decisions, and persistent state tracking. Never prompt on page load, always use a two-step flow with an in-app pre-prompt, and implement cooldowns between re-prompts. Track dismiss counts in localStorage and provide clear browser-specific instructions when permissions are denied. For the complete notification display API, see JavaScript Notifications API: Complete Tutorial.
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.