Notification and Messaging APIs
1. Notification API for Desktop Notifications
| Method/Property | Description | Browser Support |
|---|---|---|
| Notification.permission | Current permission state: "granted", "denied", or "default". |
All Modern Browsers |
| Notification.requestPermission() | Requests notification permission. Returns Promise resolving to permission state. | All Modern Browsers |
| new Notification(title, options) | Creates and displays notification. Options include body, icon, badge, tag, etc. | All Modern Browsers |
| notification.close() | Closes notification programmatically. | All Modern Browsers |
| Notification Option | Type | Description |
|---|---|---|
| body | string |
Notification body text. |
| icon | string |
URL to icon image. Typically 192x192px. |
| badge | string |
URL to badge icon for notification area. Monochrome, 96x96px. |
| tag | string |
ID for notification grouping. Same tag replaces previous notification. |
| data | any |
Arbitrary data to associate with notification. Accessible in event handlers. |
| requireInteraction | boolean |
If true, notification persists until user interacts. Default: false. |
| actions | array |
Array of action objects with action, title, icon. Service Worker only. |
| silent | boolean |
If true, no sound/vibration. Default: false. |
| vibrate | array |
Vibration pattern: [200, 100, 200] (vibrate 200ms, pause 100ms, vibrate 200ms). |
Example: Request permission and show notification
// Check if notifications are supported
if (!("Notification" in window)) {
console.log("Notifications not supported");
} else {
console.log("Current permission:", Notification.permission);
}
// Request permission
async function requestNotificationPermission() {
if (Notification.permission === "granted") {
console.log("Permission already granted");
return true;
}
if (Notification.permission === "denied") {
console.log("Permission denied - cannot request again");
return false;
}
// Request permission (must be from user gesture)
const permission = await Notification.requestPermission();
if (permission === "granted") {
console.log("Permission granted!");
return true;
} else {
console.log("Permission denied");
return false;
}
}
// Show notification
function showNotification() {
if (Notification.permission !== "granted") {
console.log("No permission to show notifications");
return;
}
const notification = new Notification("New Message", {
"body": "You have a new message from John",
"icon": "/images/icon-192.png",
"badge": "/images/badge-96.png",
"tag": "message-123", // Replace previous notification with same tag
"data": {
"messageId": "123",
"userId": "456"
},
"requireInteraction": false,
"silent": false,
"vibrate": [200, 100, 200]
});
// Event handlers
notification.onclick = (event) => {
console.log("Notification clicked:", event);
window.focus();
notification.close();
// Navigate to message
const { "messageId": messageId } = notification.data;
window.location.href = `/messages/${messageId}`;
};
notification.onclose = (event) => {
console.log("Notification closed");
};
notification.onerror = (event) => {
console.error("Notification error:", event);
};
notification.onshow = (event) => {
console.log("Notification shown");
};
// Auto-close after 5 seconds
setTimeout(() => {
notification.close();
}, 5000);
}
// Usage - must be from user interaction
document.getElementById("notifyBtn").addEventListener("click", async () => {
const hasPermission = await requestNotificationPermission();
if (hasPermission) {
showNotification();
}
});
Example: Notification with actions (Service Worker)
// service-worker.js - Show notification with actions
self.addEventListener("push", (event) => {
const data = event.data.json();
const options = {
"body": data.body,
"icon": "/images/icon-192.png",
"badge": "/images/badge-96.png",
"tag": data.tag,
"data": data,
"actions": [
{
"action": "view",
"title": "View",
"icon": "/images/view-icon.png"
},
{
"action": "dismiss",
"title": "Dismiss",
"icon": "/images/dismiss-icon.png"
}
],
"requireInteraction": true
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// Handle notification click
self.addEventListener("notificationclick", (event) => {
console.log("Notification clicked:", event.action);
event.notification.close();
if (event.action === "view") {
// Open app to specific page
event.waitUntil(
clients.openWindow(`/app?id=${event.notification.data.id}`)
);
} else if (event.action === "dismiss") {
// Just close - no action needed
console.log("Notification dismissed");
} else {
// Default click (no action button)
event.waitUntil(
clients.openWindow("/app")
);
}
});
// Handle notification close
self.addEventListener("notificationclose", (event) => {
console.log("Notification closed:", event.notification.tag);
// Track analytics
event.waitUntil(
fetch("/analytics/notification-closed", {
"method": "POST",
"body": JSON.stringify({
"tag": event.notification.tag,
"timestamp": Date.now()
})
})
);
});
Note: Notification API shows desktop notifications to users. Requires user permission - request from user gesture. Use
tag to replace/update existing notifications. Service Worker notifications support action buttons. Always handle permission denial gracefully.
Warning: Must request permission from user gesture (click). Permission is per-origin. If denied, cannot request again - user must manually enable. Don't spam notifications. Respect
requireInteraction - don't force persistent notifications. Test across browsers - behavior varies.
2. Push Messaging Registration and Handling
| Method/Property | Description | Browser Support |
|---|---|---|
| registration.pushManager.subscribe(options) | Subscribes to push notifications. Returns PushSubscription with endpoint and keys. | All Modern Browsers |
| registration.pushManager.getSubscription() | Gets existing push subscription or null if not subscribed. | All Modern Browsers |
| subscription.unsubscribe() | Unsubscribes from push notifications. | All Modern Browsers |
| subscription.endpoint | Push service URL for sending notifications. | All Modern Browsers |
| subscription.getKey(name) | Gets encryption key. Name: "p256dh" (public key) or "auth" (auth secret). |
All Modern Browsers |
Example: Subscribe to push notifications
// Subscribe to push notifications
async function subscribeToPush() {
try {
// Check support
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
throw new Error("Push notifications not supported");
}
// Register service worker
const registration = await navigator.serviceWorker.register("/sw.js");
await navigator.serviceWorker.ready;
// Check existing subscription
let subscription = await registration.pushManager.getSubscription();
if (subscription) {
console.log("Already subscribed:", subscription);
return subscription;
}
// Request notification permission
const permission = await Notification.requestPermission();
if (permission !== "granted") {
throw new Error("Notification permission denied");
}
// Subscribe to push
// VAPID public key from your server
const vapidPublicKey = "YOUR_VAPID_PUBLIC_KEY";
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
subscription = await registration.pushManager.subscribe({
"userVisibleOnly": true, // Must be true
"applicationServerKey": convertedVapidKey
});
console.log("Push subscription:", subscription);
// Send subscription to server
await fetch("/api/push/subscribe", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({
"endpoint": subscription.endpoint,
"keys": {
"p256dh": arrayBufferToBase64(subscription.getKey("p256dh")),
"auth": arrayBufferToBase64(subscription.getKey("auth"))
}
})
});
console.log("Subscription sent to server");
return subscription;
} catch (error) {
console.error("Push subscription failed:", error);
throw error;
}
}
// Helper: Convert VAPID key
function urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, "+")
.replace(/_/g, "/");
const rawData = atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// Helper: Convert ArrayBuffer to base64
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
// Unsubscribe
async function unsubscribeFromPush() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
console.log("Unsubscribed from push");
// Notify server
await fetch("/api/push/unsubscribe", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({
"endpoint": subscription.endpoint
})
});
}
}
Example: Handle push messages in service worker
// service-worker.js
self.addEventListener("push", (event) => {
console.log("Push received");
let notificationData = {
"title": "New Notification",
"body": "You have a new update",
"icon": "/images/icon-192.png"
};
// Parse push data if available
if (event.data) {
try {
notificationData = event.data.json();
} catch (e) {
notificationData.body = event.data.text();
}
}
const options = {
"body": notificationData.body,
"icon": notificationData.icon || "/images/icon-192.png",
"badge": "/images/badge-96.png",
"tag": notificationData.tag || "default",
"data": notificationData.data || {},
"actions": notificationData.actions || [],
"requireInteraction": notificationData.requireInteraction || false,
"vibrate": [200, 100, 200]
};
// Show notification
event.waitUntil(
self.registration.showNotification(notificationData.title, options)
);
});
// Handle notification click
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const urlToOpen = event.notification.data.url || "/";
event.waitUntil(
clients.matchAll({
"type": "window",
"includeUncontrolled": true
}).then((clientList) => {
// Check if already open
for (const client of clientList) {
if (client.url === urlToOpen && "focus" in client) {
return client.focus();
}
}
// Open new window
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// Handle push subscription change
self.addEventListener("pushsubscriptionchange", (event) => {
console.log("Push subscription changed");
event.waitUntil(
// Resubscribe
self.registration.pushManager.subscribe({
"userVisibleOnly": true,
"applicationServerKey": urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
}).then((subscription) => {
// Send new subscription to server
return fetch("/api/push/subscribe", {
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": JSON.stringify({
"endpoint": subscription.endpoint,
"keys": {
"p256dh": arrayBufferToBase64(subscription.getKey("p256dh")),
"auth": arrayBufferToBase64(subscription.getKey("auth"))
}
})
});
})
);
});
Note: Push API enables server-to-client push notifications. Requires Service Worker and notification permission. Use VAPID keys for authentication. Subscription includes endpoint and encryption keys - send to your server. Server uses Web Push protocol to send notifications.
Warning: Requires HTTPS and Service Worker. Must set
userVisibleOnly: true - silent push not allowed. Handle pushsubscriptionchange event - subscriptions can expire. Don't send sensitive data in push payload - encrypt or fetch from server. Test subscription renewal flow.
3. Badge API for Application Badge Updates
| Method | Description | Browser Support |
|---|---|---|
| navigator.setAppBadge(count) | Sets app icon badge count. Omit count for generic badge indicator. | Chrome, Edge, Safari |
| navigator.clearAppBadge() | Clears app icon badge. | Chrome, Edge, Safari |
Example: Update app badge
// Check if Badge API is supported
if ("setAppBadge" in navigator) {
console.log("Badge API supported");
} else {
console.log("Badge API not supported");
}
// Set badge with count
async function updateBadge(count) {
try {
if ("setAppBadge" in navigator) {
if (count > 0) {
await navigator.setAppBadge(count);
console.log(`Badge set to ${count}`);
} else {
await navigator.clearAppBadge();
console.log("Badge cleared");
}
}
} catch (error) {
console.error("Failed to update badge:", error);
}
}
// Set generic badge (no number)
async function setGenericBadge() {
try {
if ("setAppBadge" in navigator) {
await navigator.setAppBadge();
console.log("Generic badge set");
}
} catch (error) {
console.error("Failed to set badge:", error);
}
}
// Clear badge
async function clearBadge() {
try {
if ("clearAppBadge" in navigator) {
await navigator.clearAppBadge();
console.log("Badge cleared");
}
} catch (error) {
console.error("Failed to clear badge:", error);
}
}
// Example: Update badge based on unread messages
let unreadCount = 0;
function receiveMessage(message) {
unreadCount++;
updateBadge(unreadCount);
displayMessage(message);
}
function markAsRead() {
unreadCount = Math.max(0, unreadCount - 1);
updateBadge(unreadCount);
}
function markAllAsRead() {
unreadCount = 0;
clearBadge();
}
Example: Badge API in Service Worker
// service-worker.js - Update badge on push
self.addEventListener("push", (event) => {
const data = event.data.json();
// Show notification
const notificationPromise = self.registration.showNotification(
data.title,
{
"body": data.body,
"icon": "/images/icon-192.png",
"badge": "/images/badge-96.png",
"tag": data.tag,
"data": data
}
);
// Update badge
const badgePromise = (async () => {
if ("setAppBadge" in navigator) {
// Get current unread count from data
const unreadCount = data.unreadCount || 1;
await navigator.setAppBadge(unreadCount);
}
})();
event.waitUntil(
Promise.all([notificationPromise, badgePromise])
);
});
// Clear badge when notification clicked
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const clearBadgePromise = (async () => {
if ("clearAppBadge" in navigator) {
await navigator.clearAppBadge();
}
})();
const openWindowPromise = clients.openWindow(
event.notification.data.url || "/"
);
event.waitUntil(
Promise.all([clearBadgePromise, openWindowPromise])
);
});
Note: Badge API sets app icon badge - visible on dock/taskbar/home screen. Works for installed PWAs. Set number for count or no argument for generic indicator. Clear when user views content. Limited browser support but good progressive enhancement.
Warning: Only works for installed PWAs - not regular websites. Limited browser support (Chrome, Edge, Safari). Always check feature support before using. Badge persists across sessions - clear appropriately. Don't use for critical notifications - combine with Notification API.
4. Wake Lock API for Screen Wake Management
| Method/Property | Description | Browser Support |
|---|---|---|
| navigator.wakeLock.request(type) | Requests wake lock. Type: "screen". Returns WakeLockSentinel. |
Chrome, Edge |
| wakeLock.release() | Releases wake lock, allowing screen to sleep. | Chrome, Edge |
| wakeLock.released | Promise that resolves when wake lock is released. | Chrome, Edge |
| wakeLock.type | Type of wake lock ("screen"). |
Chrome, Edge |
Example: Prevent screen sleep during video playback
// Wake Lock manager
class WakeLockManager {
constructor() {
this.wakeLock = null;
this.isSupported = "wakeLock" in navigator;
}
async request() {
if (!this.isSupported) {
console.log("Wake Lock API not supported");
return false;
}
try {
this.wakeLock = await navigator.wakeLock.request("screen");
console.log("Wake lock acquired");
// Listen for release
this.wakeLock.addEventListener("release", () => {
console.log("Wake lock released");
this.wakeLock = null;
});
return true;
} catch (error) {
console.error("Wake lock request failed:", error);
return false;
}
}
async release() {
if (this.wakeLock) {
await this.wakeLock.release();
this.wakeLock = null;
console.log("Wake lock manually released");
}
}
async reacquire() {
// Reacquire if previously active (e.g., after page visibility change)
if (this.wakeLock && this.wakeLock.released) {
await this.request();
}
}
}
// Video player with wake lock
const wakeLockManager = new WakeLockManager();
const video = document.getElementById("myVideo");
video.addEventListener("play", async () => {
await wakeLockManager.request();
});
video.addEventListener("pause", async () => {
await wakeLockManager.release();
});
video.addEventListener("ended", async () => {
await wakeLockManager.release();
});
// Reacquire wake lock when page becomes visible
document.addEventListener("visibilitychange", async () => {
if (document.visibilityState === "visible" && !video.paused) {
await wakeLockManager.reacquire();
}
});
// Release on page unload
window.addEventListener("beforeunload", async () => {
await wakeLockManager.release();
});
Example: Recipe app with wake lock
// Keep screen on while cooking
class RecipeWakeLock {
constructor() {
this.wakeLock = null;
this.isActive = false;
}
async enable() {
if ("wakeLock" in navigator) {
try {
this.wakeLock = await navigator.wakeLock.request("screen");
this.isActive = true;
console.log("Screen will stay on during cooking");
// Update UI
document.getElementById("wakeLockBtn").textContent = "Turn Off Keep Awake";
document.getElementById("wakeLockStatus").textContent = "Screen will stay on";
// Handle release
this.wakeLock.addEventListener("release", () => {
console.log("Wake lock released");
this.isActive = false;
this.updateUI();
});
} catch (error) {
console.error("Failed to enable wake lock:", error);
alert("Could not keep screen on. Make sure page is visible.");
}
} else {
alert("Wake Lock not supported in this browser");
}
}
async disable() {
if (this.wakeLock) {
await this.wakeLock.release();
this.isActive = false;
this.updateUI();
}
}
async toggle() {
if (this.isActive) {
await this.disable();
} else {
await this.enable();
}
}
updateUI() {
const btn = document.getElementById("wakeLockBtn");
const status = document.getElementById("wakeLockStatus");
if (this.isActive) {
btn.textContent = "Turn Off Keep Awake";
status.textContent = "Screen will stay on";
} else {
btn.textContent = "Turn On Keep Awake";
status.textContent = "Screen may sleep";
}
}
}
// Initialize
const recipeWakeLock = new RecipeWakeLock();
document.getElementById("wakeLockBtn").addEventListener("click", () => {
recipeWakeLock.toggle();
});
// Auto-enable when starting recipe
document.getElementById("startCookingBtn").addEventListener("click", () => {
recipeWakeLock.enable();
startRecipeTimer();
});
Note: Wake Lock API prevents screen from sleeping. Use for video playback, reading, recipes, presentations, etc. Only
"screen" type currently supported. Wake lock auto-releases when page hidden or battery low. Reacquire on visibility change if needed.
Warning: Limited browser support (Chrome, Edge). Requires visible page - released when tab hidden. Can be denied by browser (low battery, user settings). Always handle request failure gracefully. Remember to release when done - drains battery. Respect user's battery.
5. Idle Detection API for User Activity Monitoring
| Method/Property | Description | Browser Support |
|---|---|---|
| new IdleDetector() | Creates idle detector instance. | Chrome, Edge (Experimental) |
| detector.start(options) | Starts monitoring. Options: threshold (ms) and signal (AbortSignal). |
Chrome, Edge (Experimental) |
| detector.userState | Current user state: "active" or "idle". |
Chrome, Edge (Experimental) |
| detector.screenState | Current screen state: "locked" or "unlocked". |
Chrome, Edge (Experimental) |
| detector.onchange | Event handler for state changes. | Chrome, Edge (Experimental) |
Example: Detect user idle state
// Check support and request permission
async function setupIdleDetection() {
// Check support
if (!("IdleDetector" in window)) {
console.log("Idle Detection API not supported");
return;
}
// Request permission
try {
const permission = await IdleDetector.requestPermission();
if (permission !== "granted") {
console.log("Idle detection permission denied");
return;
}
console.log("Idle detection permission granted");
startIdleDetection();
} catch (error) {
console.error("Permission request failed:", error);
}
}
// Start idle detection
async function startIdleDetection() {
try {
const idleDetector = new IdleDetector();
const controller = new AbortController();
// Listen for state changes
idleDetector.addEventListener("change", () => {
const userState = idleDetector.userState;
const screenState = idleDetector.screenState;
console.log(`User: ${userState}, Screen: ${screenState}`);
if (userState === "idle") {
handleUserIdle();
} else if (userState === "active") {
handleUserActive();
}
if (screenState === "locked") {
handleScreenLocked();
} else if (screenState === "unlocked") {
handleScreenUnlocked();
}
});
// Start monitoring
await idleDetector.start({
"threshold": 60000, // 60 seconds
"signal": controller.signal
});
console.log("Idle detection started (threshold: 60s)");
// Stop detection after 10 minutes
setTimeout(() => {
controller.abort();
console.log("Idle detection stopped");
}, 10 * 60 * 1000);
} catch (error) {
console.error("Idle detection failed:", error);
}
}
function handleUserIdle() {
console.log("User is idle");
// Pause background tasks, sync, etc.
pauseBackgroundSync();
showIdleMessage();
}
function handleUserActive() {
console.log("User is active");
// Resume activities
resumeBackgroundSync();
hideIdleMessage();
}
function handleScreenLocked() {
console.log("Screen locked");
// Pause video, save work, etc.
pauseMedia();
autoSaveWork();
}
function handleScreenUnlocked() {
console.log("Screen unlocked");
// Resume activities
resumeMedia();
}
// Initialize
setupIdleDetection();
Example: Auto-logout on idle
// Auto-logout after extended idle period
class IdleLogoutManager {
constructor(idleThreshold = 5 * 60 * 1000, logoutDelay = 60 * 1000) {
this.idleThreshold = idleThreshold;
this.logoutDelay = logoutDelay;
this.idleDetector = null;
this.logoutTimer = null;
this.warningShown = false;
}
async start() {
if (!("IdleDetector" in window)) {
console.log("Using fallback idle detection");
this.useFallbackDetection();
return;
}
try {
const permission = await IdleDetector.requestPermission();
if (permission !== "granted") {
this.useFallbackDetection();
return;
}
this.idleDetector = new IdleDetector();
this.idleDetector.addEventListener("change", () => {
if (this.idleDetector.userState === "idle") {
this.onIdle();
} else {
this.onActive();
}
});
await this.idleDetector.start({
"threshold": this.idleThreshold
});
console.log("Idle logout protection active");
} catch (error) {
console.error("Idle detection setup failed:", error);
this.useFallbackDetection();
}
}
onIdle() {
console.log("User idle - starting logout countdown");
// Show warning
this.showWarning();
// Start logout timer
this.logoutTimer = setTimeout(() => {
this.logout();
}, this.logoutDelay);
}
onActive() {
console.log("User active - cancelling logout");
// Cancel logout
if (this.logoutTimer) {
clearTimeout(this.logoutTimer);
this.logoutTimer = null;
}
// Hide warning
this.hideWarning();
}
showWarning() {
if (this.warningShown) return;
this.warningShown = true;
const warning = document.getElementById("idleWarning");
warning.style.display = "block";
warning.textContent = `You will be logged out in ${this.logoutDelay / 1000} seconds due to inactivity`;
}
hideWarning() {
this.warningShown = false;
const warning = document.getElementById("idleWarning");
warning.style.display = "none";
}
logout() {
console.log("Logging out due to inactivity");
// Clear session
localStorage.removeItem("authToken");
sessionStorage.clear();
// Redirect to login
window.location.href = "/login?reason=idle";
}
useFallbackDetection() {
// Fallback: use mouse/keyboard events
let lastActivity = Date.now();
const resetTimer = () => {
lastActivity = Date.now();
this.hideWarning();
};
["mousedown", "mousemove", "keypress", "scroll", "touchstart"].forEach(event => {
document.addEventListener(event, resetTimer, true);
});
// Check periodically
setInterval(() => {
const idleTime = Date.now() - lastActivity;
if (idleTime > this.idleThreshold) {
this.onIdle();
}
}, 5000);
}
}
// Initialize
const idleLogout = new IdleLogoutManager(
5 * 60 * 1000, // 5 minutes idle threshold
60 * 1000 // 1 minute warning before logout
);
idleLogout.start();
Note: Idle Detection API monitors user activity and screen lock state. Requires permission. Use for auto-logout, pause sync, save battery. Threshold is minimum idle time before detection. Experimental API - limited support.
Warning: Highly experimental - Chrome/Edge only behind flag. Requires permission. Privacy-sensitive - use responsibly. Provide fallback for unsupported browsers. Don't rely solely on this for security - implement server-side session timeout. Test thoroughly before production use.
6. Picture-in-Picture API for Video Overlay
| Method/Property | Description | Browser Support |
|---|---|---|
| video.requestPictureInPicture() | Requests PiP mode for video element. Returns PictureInPictureWindow. | All Modern Browsers |
| document.exitPictureInPicture() | Exits PiP mode. | All Modern Browsers |
| document.pictureInPictureElement | Currently active PiP element or null. | All Modern Browsers |
| document.pictureInPictureEnabled | Boolean indicating if PiP is available. | All Modern Browsers |
| pipWindow.width | Width of PiP window. | All Modern Browsers |
| pipWindow.height | Height of PiP window. | All Modern Browsers |
Example: Toggle Picture-in-Picture
// Check PiP support
if (!document.pictureInPictureEnabled) {
console.log("Picture-in-Picture not supported");
document.getElementById("pipBtn").disabled = true;
}
// Toggle PiP mode
async function togglePictureInPicture() {
const video = document.getElementById("myVideo");
try {
// Exit if already in PiP
if (document.pictureInPictureElement) {
await document.exitPictureInPicture();
console.log("Exited Picture-in-Picture");
return;
}
// Enter PiP
const pipWindow = await video.requestPictureInPicture();
console.log("Entered Picture-in-Picture");
console.log(`PiP window size: ${pipWindow.width}x${pipWindow.height}`);
// Listen for resize
pipWindow.addEventListener("resize", () => {
console.log(`PiP resized: ${pipWindow.width}x${pipWindow.height}`);
});
} catch (error) {
console.error("PiP failed:", error);
if (error.name === "NotAllowedError") {
alert("Picture-in-Picture not allowed. Check browser permissions.");
} else if (error.name === "InvalidStateError") {
alert("Video must be playing to enable Picture-in-Picture.");
}
}
}
// Add button listener
document.getElementById("pipBtn").addEventListener("click", togglePictureInPicture);
// Listen for PiP events
const video = document.getElementById("myVideo");
video.addEventListener("enterpictureinpicture", (event) => {
console.log("Entered PiP");
document.getElementById("pipBtn").textContent = "Exit PiP";
// Update UI
document.body.classList.add("pip-active");
});
video.addEventListener("leavepictureinpicture", (event) => {
console.log("Left PiP");
document.getElementById("pipBtn").textContent = "Enter PiP";
// Update UI
document.body.classList.remove("pip-active");
});
Example: Auto PiP on scroll or tab switch
// Auto-enable PiP when video scrolls out of view or tab hidden
class AutoPiPManager {
constructor(video) {
this.video = video;
this.autoPiPEnabled = true;
this.observer = null;
this.setupIntersectionObserver();
this.setupVisibilityChange();
}
setupIntersectionObserver() {
// Detect when video scrolls out of view
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!this.autoPiPEnabled) return;
// Video scrolled out of view and is playing
if (!entry.isIntersecting && !this.video.paused) {
this.enterPiP();
}
// Video back in view - exit PiP
else if (entry.isIntersecting && document.pictureInPictureElement) {
this.exitPiP();
}
});
}, {
"threshold": 0.5 // 50% visibility threshold
});
this.observer.observe(this.video);
}
setupVisibilityChange() {
// Enable PiP when switching tabs
document.addEventListener("visibilitychange", () => {
if (!this.autoPiPEnabled) return;
if (document.hidden && !this.video.paused) {
this.enterPiP();
} else if (!document.hidden && document.pictureInPictureElement) {
this.exitPiP();
}
});
}
async enterPiP() {
if (document.pictureInPictureElement) return;
try {
await this.video.requestPictureInPicture();
console.log("Auto PiP enabled");
} catch (error) {
console.error("Auto PiP failed:", error);
}
}
async exitPiP() {
if (!document.pictureInPictureElement) return;
try {
await document.exitPictureInPicture();
console.log("Auto PiP disabled");
} catch (error) {
console.error("Exit PiP failed:", error);
}
}
setAutoPiP(enabled) {
this.autoPiPEnabled = enabled;
console.log(`Auto PiP ${enabled ? "enabled" : "disabled"}`);
// Exit PiP if disabled
if (!enabled && document.pictureInPictureElement) {
this.exitPiP();
}
}
destroy() {
if (this.observer) {
this.observer.disconnect();
}
}
}
// Initialize
const video = document.getElementById("myVideo");
const autoPiP = new AutoPiPManager(video);
// Toggle auto PiP
document.getElementById("autoPiPToggle").addEventListener("change", (event) => {
autoPiP.setAutoPiP(event.target.checked);
});
Example: Custom PiP controls
// Add custom controls to PiP window (Chrome 116+)
const video = document.getElementById("myVideo");
// Check if Media Session API is available for controls
if ("mediaSession" in navigator) {
// Set metadata
navigator.mediaSession.metadata = new MediaMetadata({
"title": "My Video Title",
"artist": "Content Creator",
"album": "Video Series",
"artwork": [
{ "src": "/images/artwork-96.png", "sizes": "96x96", "type": "image/png" },
{ "src": "/images/artwork-512.png", "sizes": "512x512", "type": "image/png" }
]
});
// Set action handlers (visible in PiP)
navigator.mediaSession.setActionHandler("play", () => {
video.play();
});
navigator.mediaSession.setActionHandler("pause", () => {
video.pause();
});
navigator.mediaSession.setActionHandler("previoustrack", () => {
playPreviousVideo();
});
navigator.mediaSession.setActionHandler("nexttrack", () => {
playNextVideo();
});
navigator.mediaSession.setActionHandler("seekbackward", (details) => {
video.currentTime = Math.max(video.currentTime - (details.seekOffset || 10), 0);
});
navigator.mediaSession.setActionHandler("seekforward", (details) => {
video.currentTime = Math.min(
video.currentTime + (details.seekOffset || 10),
video.duration
);
});
}
// Update playback state
video.addEventListener("play", () => {
navigator.mediaSession.playbackState = "playing";
});
video.addEventListener("pause", () => {
navigator.mediaSession.playbackState = "paused";
});
// Update position
video.addEventListener("timeupdate", () => {
if ("setPositionState" in navigator.mediaSession) {
navigator.mediaSession.setPositionState({
"duration": video.duration,
"playbackRate": video.playbackRate,
"position": video.currentTime
});
}
});
Note: Picture-in-Picture API enables floating video overlay. Video continues playing while user browses other tabs/apps. Must be triggered by user interaction. Use for video conferencing, tutorials, live streams. Combine with Media Session API for custom controls in PiP window.
Warning: Requires user interaction to trigger (security). Some browsers require video to be playing. Can be disabled by browser or user preferences. Always check
document.pictureInPictureEnabled. Handle enter/leave events for UI updates. Only one PiP window at a time.
Notification and Messaging Best Practices
- Always request notification permission from user gesture - explain why first
- Use notification
tagto replace/update instead of spamming - Push notifications require Service Worker and VAPID keys
- Handle
pushsubscriptionchange- subscriptions can expire - Badge API only works for installed PWAs - check support
- Clear app badge when user views content - don't leave stale counts
- Wake Lock API requires visible page - reacquire after visibility change
- Release wake lock when done - respects user's battery
- Idle Detection API is experimental - provide fallback detection
- Use idle detection for auto-logout, pause sync, save battery
- PiP requires user interaction to trigger - can't auto-enable on page load
- Combine PiP with Media Session API for rich controls
- Handle all notification/PiP events for proper UI state management
- Test permission flows - handle grant, deny, and "ask later" states