🔔 Notification API
The Notification API lets your website show desktop notifications using JavaScript—similar to how apps display alerts.
👉 Notification Permissions
Because notifications can be annoying or misused, browsers enforce two rules:
Users must grant permission before any site can send notifications.
Notifications only work on HTTPS websites (secure context).
Other important points:
Users may grant, deny, or ignore the permission request.
If they deny, you cannot ask again.
If they ignore the prompt, you can request again later.
To ask for permission:
let permission = await Notification.requestPermission();Possible results:
'granted'→ user allows notifications'denied'→ user blocks notifications'default'→ user ignored the prompt (treated like denied)
🎯 Showing & Closing Notifications
To show a notification, use the Notification constructor:
const greeting = new Notification('Hi, How are you?');You can add options like body text or an icon:
const greeting = new Notification('Hi, How are you?', {
body: 'Have a good day',
icon: './img/goodday.png'
});Close a notification
greeting.close();Close it automatically after 10 seconds:
setTimeout(() => greeting.close(), 10000);📱 Notification Events
A notification can trigger these events:
show→ when it appearsclick→ when the user clicks iterror→ when something goes wrongclose→ when the user closes it or you call.close()
Attach event listeners like this:
const greeting = new Notification('Hi, How are you?', {
body: 'Have a good day',
});
greeting.addEventListener('click', () => {
window.open('<https://www.codeveda.dev/>');
});Shortcut using properties:
greeting.onclick = () => window.open('<https://www.codeveda.dev/>');🟨 Notification API example
In this example, we’ll build a simple web application that shows a desktop notification to the users.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>JavaScript Notification API</title>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<div class="container">
<h1>JavaScript Notification API Demo</h1>
<div class="error"></div>
</div>
<script src="js/app.js"></script>
</body>
</html>* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #e0f7fa, #e1bee7);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
background-color: #fff;
padding: 2rem 3rem;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 450px;
width: 100%;
}
h1 {
font-size: 1.5rem;
color: #333;
margin-bottom: 1rem;
}
.error {
display: none;
background-color: #f8d7da;
color: #721c24;
padding: 1rem;
border: 1px solid #f5c6cb;
border-radius: 8px;
margin-top: 1rem;
font-weight: bold;
}(async () => {
// create and show the notification
const showNotification = () => {
const notification = new Notification('JavaScript Notification API', {
body: 'This is a JavaScript Notification API demo',
});
// close the notification after 10 seconds
setTimeout(() => {
notification.close();
}, 10 * 1000);
// navigate to a URL when clicked
notification.addEventListener('click', () => {
window.open('<https://www.codeveda.dev/>', '_blank');
});
};
// show an error message
const showError = () => {
const error = document.querySelector('.error');
error.style.display = 'block';
error.textContent = 'You blocked the notifications';
};
// check notification permission
let granted = false;
if (Notification.permission === 'granted') {
granted = true;
} else if (Notification.permission !== 'denied') {
let permission = await Notification.requestPermission();
granted = permission === 'granted';
}
// show notification or error
granted ? showNotification() : showError();
})();When you open the page, it’ll request notification permission.
If you click the allow button, you’ll see the following notification on your desktop :
If you click the notification, it’ll open the URL
https://www.codeveda.dev/:
<div class="infographic-notifapi">
<style>
.infographic-notifapi {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--demo-error-fg: #721c24;
--demo-error-bg: #f8d7da;
--demo-error-bd: #f5c6cb;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-notifapi[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
}
.infographic-notifapi * { box-sizing: border-box; }
.infographic-notifapi h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-notifapi p { margin: 0; }
.infographic-notifapi button { font-family: inherit; }
.na-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.na-icon svg { width: 100%; height: 100%; display: block; }
.na-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.na-hero { text-align: center; margin-bottom: 1.3rem; }
.na-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.na-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
/* --- demo stage --- */
.na-stage { position: relative; max-width: 400px; margin: 0 auto 1rem; }
.na-card { background: #fff; border-radius: 12px; padding: 1.8rem 1.6rem; box-shadow: 0 10px 25px rgba(0,0,0,0.1); text-align: center; }
.infographic-notifapi[data-theme="dark"] .na-card { background: #1a1a1a; box-shadow: 0 10px 25px rgba(0,0,0,0.4); }
.na-card h2 { font-size: 1.05rem; color: #333; margin-bottom: 0.9rem; font-family: 'Inter', system-ui, sans-serif; font-weight: 700; }
.infographic-notifapi[data-theme="dark"] .na-card h2 { color: #eee; }
.na-btn { padding: 0.65rem 1.3rem; background-color: var(--accent); border: none; border-radius: 8px; color: #fff;
font-size: 0.85rem; cursor: pointer; display: inline-flex; align-items: center; gap: 0.4rem; }
.na-btn:hover { opacity: 0.9; }
.na-btn .na-icon { width: 14px; height: 14px; }
.na-error { display: none; background-color: var(--demo-error-bg); color: var(--demo-error-fg); padding: 0.85rem;
border: 1px solid var(--demo-error-bd); border-radius: 8px; margin-top: 0.9rem; font-weight: 700; font-size: 0.8rem; }
.na-error.is-shown { display: block; }
/* --- notification bubble --- */
.na-bubble { position: absolute; top: -0.6rem; right: -0.6rem; width: 250px; background: var(--paper);
border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 8px 20px rgba(0,0,0,0.18); padding: 0.75rem 0.85rem;
text-align: left; cursor: pointer; opacity: 0; transform: translateY(-14px) scale(0.96); pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease; }
.na-bubble.is-shown { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
.na-bubble-head { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.3rem; }
.na-bubble-head .na-icon { width: 14px; height: 14px; color: var(--accent); }
.na-bubble-title { font-size: 0.76rem; font-weight: 700; color: var(--ink); flex: 1; }
.na-bubble-close { background: none; border: none; color: var(--ink-soft); cursor: pointer; font-size: 0.9rem; line-height: 1; padding: 0 0.1rem; }
.na-bubble-body { font-size: 0.72rem; color: var(--ink-soft); margin-bottom: 0.4rem; }
.na-bubble-timer { font-family: var(--mono-font); font-size: 0.6rem; color: var(--ink-soft); opacity: 0.7; }
@media (max-width: 460px) { .na-bubble { width: calc(100% - 1.2rem); right: 0.6rem; } }
.na-sim-row { display: flex; gap: 0.5rem; justify-content: center; margin-bottom: 1.3rem; }
.na-sim-btn { display: flex; align-items: center; gap: 0.3rem; border: 1px solid var(--line); background: var(--paper);
border-radius: 6px; padding: 0.35rem 0.7rem; font-size: 0.68rem; font-weight: 600; cursor: pointer; color: var(--ink-soft); }
.na-sim-btn:hover { border-color: var(--accent); color: var(--accent); }
.na-sim-btn .na-icon { width: 12px; height: 12px; }
/* --- console --- */
.na-console-panel { max-width: 520px; margin: 0 auto 1.2rem; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.na-console-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--line); background: var(--paper-alt); font-family: var(--mono-font); font-size: 0.7rem;
font-weight: 700; color: #888; }
.na-clear-btn { background: none; border: none; color: var(--ink-soft); font-family: var(--mono-font); font-size: 0.64rem;
cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 4px; }
.na-clear-btn:hover { color: var(--accent); background: var(--accent-soft); }
.na-console { min-height: 6.5rem; max-height: 9rem; overflow-y: auto; background: #1e1e1e; padding: 0.5rem 0.7rem; }
.na-console-line { font-family: var(--mono-font); font-size: 0.66rem; color: #d4d4d4; padding: 0.08rem 0; animation: na-pop-in 0.2s ease; white-space: pre-wrap; }
.na-console-line.is-echo { color: #7fb0ff; }
.na-console-line.is-good { color: #9fd9a8; padding-left: 0.9rem; }
.na-console-line.is-bad { color: #e88; padding-left: 0.9rem; }
.na-console-empty { font-family: var(--mono-font); font-size: 0.66rem; color: #6b6b6b; }
@keyframes na-pop-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
/* --- legend --- */
.na-legend { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; max-width: 620px; margin: 0 auto; }
.na-chip { display: flex; align-items: center; gap: 0.35rem; border: 1px solid var(--line); border-radius: 20px;
padding: 0.32rem 0.7rem; font-family: var(--mono-font); font-size: 0.62rem; color: var(--ink-soft);
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease; }
.na-chip .na-icon { width: 11px; height: 11px; }
.na-chip.is-firing { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 700; }
.na-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.4rem; max-width: 560px; margin: 0 auto; }
.na-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-notifapi *, .infographic-notifapi *::before, .infographic-notifapi *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="na-wrap">
<div class="na-stage">
<div class="na-card">
<h2>JavaScript Notification API Demo</h2>
<button class="na-btn" id="naShowBtn" type="button">
<span class="na-icon" id="naBtnIcon"></span>Request permission & show
</button>
<div class="na-error" id="naError">You blocked the notifications</div>
</div>
<div class="na-bubble" id="naBubble">
<div class="na-bubble-head">
<span class="na-icon" id="naBubbleIcon"></span>
<span class="na-bubble-title">JavaScript Notification API</span>
<button class="na-bubble-close" id="naBubbleClose" type="button">✕</button>
</div>
<div class="na-bubble-body">This is a JavaScript Notification API demo</div>
<div class="na-bubble-timer" id="naBubbleTimer">closes in 10s — click to open codeveda.dev</div>
</div>
</div>
<div class="na-sim-row">
<button class="na-sim-btn" id="naSimAllow" type="button"><span class="na-icon" id="naSimAllowIcon"></span>simulate allow</button>
<button class="na-sim-btn" id="naSimDeny" type="button"><span class="na-icon" id="naSimDenyIcon"></span>simulate deny</button>
</div>
<div class="na-console-panel">
<div class="na-console-head">
<span>>_ event log</span>
<button class="na-clear-btn" id="naClearBtn" type="button">clear</button>
</div>
<div class="na-console" id="naConsole">
<div class="na-console-empty">// click the button to begin</div>
</div>
</div>
<div class="na-legend">
<span class="na-chip" data-evt="check"><span class="na-icon" id="naChipIcon0"></span>Notification.permission</span>
<span class="na-chip" data-evt="request"><span class="na-icon" id="naChipIcon1"></span>requestPermission()</span>
<span class="na-chip" data-evt="show"><span class="na-icon" id="naChipIcon2"></span>showNotification()</span>
<span class="na-chip" data-evt="error"><span class="na-icon" id="naChipIcon3"></span>showError()</span>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-notifapi')
|| document.querySelector('.infographic-notifapi');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') {
dark = true;
} else if (bodyAttr === 'false') {
dark = false;
} else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const ICONS = {
bell: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M11.68 2.009A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673c-.824-.85-1.678-1.731-2.21-3.348"/><circle cx="18" cy="5" r="3"/></svg>',
shield: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/></svg>',
check: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.801 10A10 10 0 1 1 17 3.335"/><path d="m9 11 3 3L22 4"/></svg>',
xcircle: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>',
play: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" stroke="none"><polygon points="6 3 20 12 6 21 6 3"/></svg>'
};
wrapper.querySelector('#naBtnIcon').innerHTML = ICONS.bell;
wrapper.querySelector('#naBubbleIcon').innerHTML = ICONS.bell;
wrapper.querySelector('#naSimAllowIcon').innerHTML = ICONS.play;
wrapper.querySelector('#naSimDenyIcon').innerHTML = ICONS.play;
wrapper.querySelector('#naChipIcon0').innerHTML = ICONS.shield;
wrapper.querySelector('#naChipIcon1').innerHTML = ICONS.shield;
wrapper.querySelector('#naChipIcon2').innerHTML = ICONS.check;
wrapper.querySelector('#naChipIcon3').innerHTML = ICONS.xcircle;
const showBtn = wrapper.querySelector('#naShowBtn');
const errorEl = wrapper.querySelector('#naError');
const bubble = wrapper.querySelector('#naBubble');
const bubbleClose = wrapper.querySelector('#naBubbleClose');
const bubbleTimer = wrapper.querySelector('#naBubbleTimer');
const simAllow = wrapper.querySelector('#naSimAllow');
const simDeny = wrapper.querySelector('#naSimDeny');
const consoleEl = wrapper.querySelector('#naConsole');
const clearBtn = wrapper.querySelector('#naClearBtn');
let closeTimeout = null;
let tickInterval = null;
function logLine(text, cls) {
if (consoleEl.querySelector('.na-console-empty')) consoleEl.innerHTML = '';
const line = document.createElement('div');
line.className = 'na-console-line ' + (cls || '');
line.textContent = text;
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
function flashChip(evt) {
const chip = wrapper.querySelector('.na-chip[data-evt="' + evt + '"]');
if (!chip) return;
chip.classList.add('is-firing');
clearTimeout(chip._t);
chip._t = setTimeout(() => chip.classList.remove('is-firing'), 500);
}
function showBubble() {
errorEl.classList.remove('is-shown');
bubble.classList.add('is-shown');
logLine("new Notification('JavaScript Notification API', {...})", 'is-good');
flashChip('show');
let seconds = 10;
bubbleTimer.textContent = 'closes in ' + seconds + 's \u2014 click to open codeveda.dev';
clearInterval(tickInterval);
tickInterval = setInterval(() => {
seconds -= 1;
if (seconds <= 0) { clearInterval(tickInterval); return; }
bubbleTimer.textContent = 'closes in ' + seconds + 's \u2014 click to open codeveda.dev';
}, 1000);
clearTimeout(closeTimeout);
closeTimeout = setTimeout(() => {
bubble.classList.remove('is-shown');
clearInterval(tickInterval);
logLine('notification.close() \u25b8 auto-closed after 10s', 'is-good');
}, 10000);
}
function showErrorState() {
bubble.classList.remove('is-shown');
errorEl.classList.add('is-shown');
logLine('showError() \u25b8 "You blocked the notifications"', 'is-bad');
flashChip('error');
}
bubble.addEventListener('click', (e) => {
if (e.target === bubbleClose) return;
logLine('notification click \u25b8 window.open("https://www.codeveda.dev/")', 'is-good');
window.open('https://www.codeveda.dev/', '_blank');
});
bubbleClose.addEventListener('click', (e) => {
e.stopPropagation();
bubble.classList.remove('is-shown');
clearTimeout(closeTimeout);
clearInterval(tickInterval);
logLine('notification.close() \u25b8 closed by user', 'is-good');
});
showBtn.addEventListener('click', async () => {
logLine('> Notification.permission', 'is-echo');
if (typeof Notification === 'undefined') {
logLine('Notification API unavailable in this embedded context', 'is-bad');
logLine('// try the simulate buttons below instead', 'is-echo');
return;
}
flashChip('check');
let granted = false;
try {
if (Notification.permission === 'granted') {
granted = true;
logLine('granted (already)', 'is-good');
} else if (Notification.permission !== 'denied') {
logLine('> await Notification.requestPermission();', 'is-echo');
flashChip('request');
const permission = await Notification.requestPermission();
granted = permission === 'granted';
logLine(permission, granted ? 'is-good' : 'is-bad');
} else {
logLine('denied (already)', 'is-bad');
}
} catch (err) {
logLine('blocked or unavailable in this embed', 'is-bad');
logLine('// try the simulate buttons below instead', 'is-echo');
return;
}
if (granted) {
try {
const notification = new Notification('JavaScript Notification API', {
body: 'This is a JavaScript Notification API demo'
});
logLine('new Notification(...) \u25b8 real OS notification requested', 'is-good');
flashChip('show');
setTimeout(() => notification.close(), 10000);
notification.addEventListener('click', () => window.open('https://www.codeveda.dev/', '_blank'));
} catch (err) {
logLine('creating Notification failed in this embed \u2014 showing simulated bubble', 'is-bad');
showBubble();
}
} else {
showErrorState();
}
});
simAllow.addEventListener('click', () => {
logLine('> Notification.requestPermission(); (simulated)', 'is-echo');
flashChip('request');
showBubble();
});
simDeny.addEventListener('click', () => {
logLine('> Notification.requestPermission(); (simulated)', 'is-echo');
flashChip('request');
showErrorState();
});
clearBtn.addEventListener('click', () => {
consoleEl.innerHTML = '<div class="na-console-empty">// click the button to begin</div>';
});
})();
</script>