Browser APIs You Should Know About
A practical tour of essential browser APIs including IntersectionObserver, Clipboard, Notifications, Web Share, Page Visibility, and Performance API with working examples.
Browser APIs You Should Know About
Overview
The browser is one of the most capable application platforms ever built, and most developers barely scratch the surface of what it offers. Beyond the DOM and fetch, there is a rich collection of APIs that let you observe element visibility, copy to the clipboard, share content natively, track performance, communicate across tabs, and much more — all without a single third-party library.
I have spent years watching teams install npm packages for things the browser already does natively. A 40 KB intersection library when IntersectionObserver exists. A clipboard polyfill when the Clipboard API has near-universal support. A custom tab-sync solution when BroadcastChannel is built right in.
This article is a practical tour through the browser APIs that I believe every frontend developer should know. We will cover nineteen APIs with working code, then tie several of them together in a complete image gallery example. Every code snippet uses var and function() syntax for maximum compatibility.
Prerequisites
- Solid understanding of JavaScript fundamentals (DOM manipulation, events, callbacks)
- Familiarity with asynchronous JavaScript (Promises, callbacks)
- A modern browser (Chrome 80+, Firefox 75+, Safari 14+, Edge 80+)
- Basic HTML and CSS knowledge
- A local development server (many APIs require HTTPS or localhost)
IntersectionObserver for Lazy Loading and Infinite Scroll
IntersectionObserver tells you when an element enters or exits the viewport. Before this API, developers used scroll event listeners with getBoundingClientRect() calls — a recipe for janky performance.
var images = document.querySelectorAll('img[data-src]');
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
}, {
rootMargin: '200px 0px',
threshold: 0.01
});
images.forEach(function(img) {
observer.observe(img);
});
The rootMargin of 200px starts loading images before they scroll into view. For infinite scroll, observe a sentinel element at the bottom of your list:
var sentinel = document.getElementById('scroll-sentinel');
var scrollObserver = new IntersectionObserver(function(entries) {
if (entries[0].isIntersecting) {
loadNextPage();
}
}, { threshold: 1.0 });
scrollObserver.observe(sentinel);
This is dramatically more efficient than scroll event listeners because the browser handles the geometry calculations internally and batches callbacks.
ResizeObserver for Responsive Components
Media queries work for viewport-level responsiveness, but ResizeObserver gives you component-level responsiveness. When a container changes size — regardless of why — you get notified.
var container = document.getElementById('dashboard-panel');
var resizeObserver = new ResizeObserver(function(entries) {
var entry = entries[0];
var width = entry.contentRect.width;
if (width < 400) {
container.classList.add('compact');
container.classList.remove('wide');
} else {
container.classList.add('wide');
container.classList.remove('compact');
}
});
resizeObserver.observe(container);
This is the foundation of container queries before CSS container queries existed. I still use it for cases where I need JavaScript logic — not just style changes — when a component resizes.
MutationObserver for DOM Change Tracking
MutationObserver watches for changes to the DOM tree. This is invaluable when you are integrating with third-party widgets, building browser extensions, or need to react to DOM modifications you do not control.
var targetNode = document.getElementById('dynamic-content');
var mutationObserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
console.log('Nodes added:', mutation.addedNodes.length);
console.log('Nodes removed:', mutation.removedNodes.length);
}
if (mutation.type === 'attributes') {
console.log('Attribute changed:', mutation.attributeName);
}
});
});
mutationObserver.observe(targetNode, {
childList: true,
attributes: true,
subtree: true,
characterData: true
});
A word of caution: observing with subtree: true on a large DOM tree can be expensive. Be specific about what you observe.
Clipboard API (Read and Write)
The modern Clipboard API replaces the old document.execCommand('copy') approach. It is asynchronous and returns Promises.
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
showToast('Copied to clipboard');
}).catch(function(err) {
console.error('Clipboard write failed:', err);
fallbackCopy(text);
});
}
function readFromClipboard() {
navigator.clipboard.readText().then(function(text) {
document.getElementById('paste-target').value = text;
}).catch(function(err) {
console.error('Clipboard read failed:', err);
});
}
function fallbackCopy(text) {
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
Reading from the clipboard requires explicit user permission. Writing generally works without a prompt as long as the action is triggered by a user gesture (click, keypress).
Notification API with Permission Handling
Push-style notifications from the browser are powerful for alerting users to updates. Permission handling is the tricky part — you only get one chance to ask on most browsers, so do not ask on page load.
function requestNotificationPermission() {
if (!('Notification' in window)) {
console.log('Notifications not supported');
return;
}
if (Notification.permission === 'granted') {
return Promise.resolve('granted');
}
if (Notification.permission === 'denied') {
console.log('Notifications were previously denied');
return Promise.resolve('denied');
}
return Notification.requestPermission();
}
function showNotification(title, body, icon) {
if (Notification.permission !== 'granted') return;
var notification = new Notification(title, {
body: body,
icon: icon || '/images/default-icon.png',
badge: '/images/badge.png',
tag: 'content-update',
renotify: true
});
notification.onclick = function() {
window.focus();
notification.close();
};
setTimeout(function() {
notification.close();
}, 5000);
}
The tag property prevents duplicate notifications. Setting renotify: true means the user still gets alerted even when replacing a notification with the same tag.
Geolocation API
The Geolocation API provides the device's geographic position. It always requires user permission.
function getCurrentPosition() {
if (!navigator.geolocation) {
console.log('Geolocation not supported');
return;
}
navigator.geolocation.getCurrentPosition(
function(position) {
var lat = position.coords.latitude;
var lng = position.coords.longitude;
var accuracy = position.coords.accuracy;
console.log('Location:', lat, lng, '(accuracy: ' + accuracy + 'm)');
},
function(error) {
switch (error.code) {
case error.PERMISSION_DENIED:
console.log('User denied geolocation');
break;
case error.POSITION_UNAVAILABLE:
console.log('Position unavailable');
break;
case error.TIMEOUT:
console.log('Request timed out');
break;
}
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000
}
);
}
Set maximumAge to avoid repeated GPS lookups. A value of 300000 (five minutes) is reasonable for most use cases.
Web Share API for Native Sharing
The Web Share API invokes the platform's native share dialog. It works on mobile browsers and some desktop browsers. It must be triggered by a user gesture.
function shareContent(title, text, url) {
if (!navigator.share) {
fallbackShare(url);
return;
}
navigator.share({
title: title,
text: text,
url: url
}).then(function() {
console.log('Content shared successfully');
}).catch(function(err) {
if (err.name !== 'AbortError') {
console.error('Share failed:', err);
}
});
}
function fallbackShare(url) {
navigator.clipboard.writeText(url).then(function() {
showToast('Link copied to clipboard');
});
}
Always catch the AbortError — it fires when the user cancels the share dialog, and that is perfectly normal behavior.
Page Visibility API for Tab Management
The Page Visibility API tells you when the user switches away from your tab. Use it to pause expensive operations, stop polling, or pause media.
var pollInterval = null;
function startPolling() {
pollInterval = setInterval(function() {
fetchUpdates();
}, 30000);
}
function stopPolling() {
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
}
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
stopPolling();
pauseAnimations();
} else {
startPolling();
resumeAnimations();
fetchUpdates(); // Catch up immediately
}
});
This is one of the most underused APIs. If your application polls a server or runs animations, you are wasting resources when the tab is not visible.
Broadcast Channel for Cross-Tab Communication
BroadcastChannel allows communication between tabs, windows, and iframes of the same origin. It is simpler than SharedWorker or localStorage event hacking.
var channel = new BroadcastChannel('app-sync');
// Send messages
function broadcastLogout() {
channel.postMessage({ type: 'logout', timestamp: Date.now() });
}
function broadcastThemeChange(theme) {
channel.postMessage({ type: 'theme-change', theme: theme });
}
// Receive messages
channel.onmessage = function(event) {
var data = event.data;
switch (data.type) {
case 'logout':
window.location.href = '/login';
break;
case 'theme-change':
document.body.className = data.theme;
break;
case 'data-update':
refreshData(data.payload);
break;
}
};
I use this for syncing authentication state across tabs. When a user logs out in one tab, every other tab should log out too.
Performance API (Mark, Measure, Navigation Timing)
The Performance API gives you high-resolution timing data. It is far more useful than sprinkling Date.now() throughout your code.
// Custom performance marks
performance.mark('api-call-start');
fetchData().then(function(data) {
performance.mark('api-call-end');
performance.measure('api-call-duration', 'api-call-start', 'api-call-end');
var measure = performance.getEntriesByName('api-call-duration')[0];
console.log('API call took:', measure.duration.toFixed(2) + 'ms');
performance.clearMarks();
performance.clearMeasures();
});
// Navigation timing
function getPageLoadMetrics() {
var nav = performance.getEntriesByType('navigation')[0];
return {
dnsLookup: nav.domainLookupEnd - nav.domainLookupStart,
tcpConnect: nav.connectEnd - nav.connectStart,
serverResponse: nav.responseStart - nav.requestStart,
domParsing: nav.domContentLoadedEventEnd - nav.responseEnd,
totalLoad: nav.loadEventEnd - nav.startTime
};
}
// Resource timing
function getSlowResources(threshold) {
var resources = performance.getEntriesByType('resource');
return resources.filter(function(r) {
return r.duration > (threshold || 500);
}).map(function(r) {
return { name: r.name, duration: r.duration.toFixed(2) + 'ms', type: r.initiatorType };
});
}
Send these metrics to your analytics backend. Real user performance data is worth more than any synthetic benchmark.
URL and URLSearchParams
Stop parsing query strings with regex. URL and URLSearchParams handle this cleanly.
var url = new URL('https://example.com/search?q=javascript&page=2&sort=date');
console.log(url.hostname); // 'example.com'
console.log(url.pathname); // '/search'
console.log(url.searchParams.get('q')); // 'javascript'
console.log(url.searchParams.get('page')); // '2'
// Modify parameters
url.searchParams.set('page', '3');
url.searchParams.append('filter', 'recent');
url.searchParams.delete('sort');
console.log(url.toString());
// 'https://example.com/search?q=javascript&page=3&filter=recent'
// Build query strings from scratch
var params = new URLSearchParams();
params.set('search', 'browser apis');
params.set('limit', '20');
console.log(params.toString()); // 'search=browser+apis&limit=20'
URLSearchParams handles encoding automatically. No more manual encodeURIComponent calls for query parameters.
AbortController Beyond Fetch
Most developers know AbortController for cancelling fetch requests. But it works with any API that accepts an AbortSignal, and you can use it for your own abortable operations.
var controller = new AbortController();
// Cancel fetch
fetch('/api/data', { signal: controller.signal }).catch(function(err) {
if (err.name === 'AbortError') {
console.log('Fetch cancelled');
}
});
// Cancel event listeners
var buttonController = new AbortController();
document.getElementById('my-button').addEventListener('click', function() {
console.log('Clicked');
}, { signal: buttonController.signal });
// Remove the listener later without needing a named reference
buttonController.abort();
// Timeout pattern
function fetchWithTimeout(url, timeoutMs) {
var controller = new AbortController();
var timeoutId = setTimeout(function() {
controller.abort();
}, timeoutMs);
return fetch(url, { signal: controller.signal }).then(function(response) {
clearTimeout(timeoutId);
return response;
});
}
The event listener removal trick alone is worth knowing. No more storing function references just to call removeEventListener later.
Fullscreen API
The Fullscreen API lets you display any element in fullscreen mode. It is essential for media viewers, presentations, and immersive experiences.
function enterFullscreen(element) {
if (element.requestFullscreen) {
return element.requestFullscreen();
} else if (element.webkitRequestFullscreen) {
return element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
return element.msRequestFullscreen();
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
return document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
return document.webkitExitFullscreen();
}
}
function toggleFullscreen(element) {
if (document.fullscreenElement) {
exitFullscreen();
} else {
enterFullscreen(element);
}
}
document.addEventListener('fullscreenchange', function() {
var isFullscreen = !!document.fullscreenElement;
document.getElementById('fullscreen-btn').textContent =
isFullscreen ? 'Exit Fullscreen' : 'Fullscreen';
});
Screen Orientation API
Lock the screen orientation for games, video players, or any experience that requires a specific orientation.
function lockLandscape() {
if (screen.orientation && screen.orientation.lock) {
screen.orientation.lock('landscape').catch(function(err) {
console.log('Orientation lock failed:', err.message);
});
}
}
screen.orientation.addEventListener('change', function() {
console.log('Orientation:', screen.orientation.type);
console.log('Angle:', screen.orientation.angle);
});
Orientation locking only works in fullscreen mode on most browsers. Plan your UX accordingly.
Vibration API for Mobile
Simple haptic feedback for mobile web applications. Use sparingly — nobody enjoys a vibrating web page.
function vibrateOnce() {
if (navigator.vibrate) {
navigator.vibrate(50);
}
}
function vibratePattern() {
// vibrate 100ms, pause 50ms, vibrate 100ms
if (navigator.vibrate) {
navigator.vibrate([100, 50, 100]);
}
}
function stopVibration() {
if (navigator.vibrate) {
navigator.vibrate(0);
}
}
I use this for form validation feedback on mobile: a quick 30ms vibrate when a field fails validation feels natural without being annoying.
Battery Status API
The Battery Status API provides information about the device battery. Note that this API has been deprecated in some browsers due to privacy concerns, but it remains available in Chromium-based browsers.
function getBatteryInfo() {
if (!navigator.getBattery) {
console.log('Battery API not supported');
return;
}
navigator.getBattery().then(function(battery) {
console.log('Level:', (battery.level * 100).toFixed(0) + '%');
console.log('Charging:', battery.charging);
console.log('Time to full:', battery.chargingTime + 's');
console.log('Time to empty:', battery.dischargingTime + 's');
battery.addEventListener('levelchange', function() {
if (battery.level < 0.15 && !battery.charging) {
enablePowerSaveMode();
}
});
});
}
A practical use case: reduce animation complexity and polling frequency when battery is low.
Network Information API
The Network Information API exposes the type and quality of the network connection.
function getNetworkInfo() {
var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (!connection) {
console.log('Network Information API not supported');
return null;
}
return {
type: connection.effectiveType, // '4g', '3g', '2g', 'slow-2g'
downlink: connection.downlink, // Mbps estimate
rtt: connection.rtt, // Round-trip time in ms
saveData: connection.saveData // User's data-saver preference
};
}
function adaptToNetwork() {
var info = getNetworkInfo();
if (!info) return;
if (info.saveData || info.type === '2g' || info.type === 'slow-2g') {
disableAutoplay();
loadLowResImages();
reducePollingFrequency();
}
var connection = navigator.connection;
connection.addEventListener('change', function() {
adaptToNetwork();
});
}
Adaptive loading based on network quality is a genuine user experience improvement, especially for users in areas with unreliable connectivity.
Web Animations API
The Web Animations API provides JavaScript control over CSS animations without requiring a library like GreenSock.
function animateElement(element) {
var animation = element.animate([
{ transform: 'translateY(0px)', opacity: 1 },
{ transform: 'translateY(-20px)', opacity: 0.5 },
{ transform: 'translateY(0px)', opacity: 1 }
], {
duration: 600,
easing: 'ease-in-out',
iterations: 1
});
animation.onfinish = function() {
console.log('Animation complete');
};
return animation;
}
function fadeIn(element, duration) {
return element.animate([
{ opacity: 0 },
{ opacity: 1 }
], {
duration: duration || 300,
fill: 'forwards'
});
}
// Pause, resume, reverse
var runningAnimation = animateElement(document.getElementById('box'));
runningAnimation.pause();
runningAnimation.play();
runningAnimation.reverse();
The advantage over CSS animations is programmatic control — you can pause, reverse, adjust playback rate, and respond to finish events.
Drag and Drop API Basics
The native Drag and Drop API is verbose but functional. Here is a minimal working example.
function setupDragAndDrop() {
var draggables = document.querySelectorAll('.draggable');
var dropZone = document.getElementById('drop-zone');
draggables.forEach(function(item) {
item.setAttribute('draggable', 'true');
item.addEventListener('dragstart', function(e) {
e.dataTransfer.setData('text/plain', item.id);
e.dataTransfer.effectAllowed = 'move';
item.classList.add('dragging');
});
item.addEventListener('dragend', function() {
item.classList.remove('dragging');
});
});
dropZone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', function() {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', function(e) {
e.preventDefault();
dropZone.classList.remove('drag-over');
var id = e.dataTransfer.getData('text/plain');
var element = document.getElementById(id);
if (element) {
dropZone.appendChild(element);
}
});
}
The key thing to remember: you must call e.preventDefault() in the dragover handler, otherwise drop will not fire.
Feature Detection Patterns
Never assume an API exists. Always check before using it. Here is a utility pattern I use across projects:
var BrowserSupport = {
intersectionObserver: 'IntersectionObserver' in window,
resizeObserver: 'ResizeObserver' in window,
mutationObserver: 'MutationObserver' in window,
clipboard: !!(navigator.clipboard && navigator.clipboard.writeText),
notifications: 'Notification' in window,
geolocation: 'geolocation' in navigator,
share: 'share' in navigator,
pageVisibility: 'hidden' in document,
broadcastChannel: 'BroadcastChannel' in window,
performanceMark: !!(window.performance && window.performance.mark),
abortController: 'AbortController' in window,
fullscreen: !!(document.fullscreenEnabled || document.webkitFullscreenEnabled),
vibration: 'vibrate' in navigator,
battery: 'getBattery' in navigator,
networkInfo: !!(navigator.connection || navigator.mozConnection),
webAnimations: 'animate' in HTMLElement.prototype,
dragAndDrop: 'draggable' in document.createElement('div'),
check: function(feature) {
if (this[feature] === undefined) {
console.warn('Unknown feature:', feature);
return false;
}
return this[feature];
},
report: function() {
var self = this;
var keys = Object.keys(this).filter(function(k) {
return typeof self[k] === 'boolean';
});
keys.forEach(function(key) {
console.log(key + ':', self[key] ? 'supported' : 'not supported');
});
}
};
Call BrowserSupport.report() during development to see what your target browser supports. Wrap API usage with BrowserSupport.check() to degrade gracefully.
Complete Working Example: Image Gallery
This gallery combines IntersectionObserver for lazy loading, Fullscreen API for immersive viewing, Web Share API for sharing, Clipboard API for copying links, Page Visibility to pause/resume a slideshow, and the Notification API for new content alerts.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Browser API Gallery</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #1a1a2e; color: #eee; }
.gallery-header { padding: 20px; text-align: center; background: #16213e; }
.gallery-header h1 { margin-bottom: 10px; }
.controls { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.controls button { padding: 8px 16px; border: 1px solid #0f3460; background: #0f3460; color: #eee; border-radius: 4px; cursor: pointer; }
.controls button:hover { background: #1a4a8a; }
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; padding: 20px; }
.gallery-item { position: relative; border-radius: 8px; overflow: hidden; background: #16213e; cursor: pointer; }
.gallery-item img { width: 100%; height: 200px; object-fit: cover; opacity: 0; transition: opacity 0.4s; }
.gallery-item img.loaded { opacity: 1; }
.gallery-item .placeholder { width: 100%; height: 200px; background: #2a2a4a; display: flex; align-items: center; justify-content: center; color: #666; }
.gallery-item .info { padding: 12px; }
.gallery-item .actions { display: flex; gap: 8px; padding: 0 12px 12px; }
.gallery-item .actions button { padding: 4px 10px; border: 1px solid #333; background: transparent; color: #aaa; border-radius: 4px; cursor: pointer; font-size: 12px; }
.gallery-item .actions button:hover { background: #333; color: #fff; }
.fullscreen-viewer { display: none; position: fixed; inset: 0; background: #000; z-index: 1000; align-items: center; justify-content: center; }
.fullscreen-viewer.active { display: flex; }
.fullscreen-viewer img { max-width: 90%; max-height: 90%; object-fit: contain; }
.fullscreen-viewer .close-btn { position: absolute; top: 20px; right: 20px; background: rgba(255,255,255,0.2); border: none; color: #fff; font-size: 24px; width: 40px; height: 40px; border-radius: 50%; cursor: pointer; }
.toast { position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; background: #0f3460; color: #fff; border-radius: 6px; display: none; z-index: 2000; }
.toast.visible { display: block; }
.slideshow-indicator { position: fixed; top: 10px; right: 10px; padding: 6px 12px; background: rgba(15,52,96,0.8); border-radius: 4px; font-size: 12px; display: none; }
.slideshow-indicator.active { display: block; }
</style>
</head>
<body>
<div class="gallery-header">
<h1>Browser API Gallery</h1>
<p>A demo combining IntersectionObserver, Fullscreen, Web Share, Clipboard, Page Visibility, and Notifications</p>
<div class="controls">
<button id="btn-slideshow">Start Slideshow</button>
<button id="btn-notify">Enable Notifications</button>
<button id="btn-fullscreen">Fullscreen Gallery</button>
</div>
</div>
<div id="gallery" class="gallery-grid"></div>
<div id="viewer" class="fullscreen-viewer">
<button class="close-btn" id="btn-close-viewer">×</button>
<img id="viewer-img" src="" alt="Full size view">
</div>
<div id="toast" class="toast"></div>
<div id="slideshow-indicator" class="slideshow-indicator">Slideshow running</div>
<script>
(function() {
// ---- Configuration ----
var IMAGES = [];
var i;
for (i = 1; i <= 24; i++) {
IMAGES.push({
id: i,
src: 'https://picsum.photos/600/400?random=' + i,
thumb: 'https://picsum.photos/300/200?random=' + i,
title: 'Photo ' + i,
description: 'A beautiful image from the gallery collection.'
});
}
var slideshowInterval = null;
var slideshowIndex = 0;
var isSlideShowRunning = false;
// ---- Toast Helper ----
function showToast(message, duration) {
var toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('visible');
setTimeout(function() {
toast.classList.remove('visible');
}, duration || 2500);
}
// ---- Render Gallery ----
function renderGallery() {
var gallery = document.getElementById('gallery');
var html = '';
IMAGES.forEach(function(img) {
html += '<div class="gallery-item" data-id="' + img.id + '">';
html += ' <div class="placeholder">Loading...</div>';
html += ' <img data-src="' + img.thumb + '" alt="' + img.title + '">';
html += ' <div class="info"><strong>' + img.title + '</strong><br><small>' + img.description + '</small></div>';
html += ' <div class="actions">';
html += ' <button class="action-view" data-src="' + img.src + '">View</button>';
html += ' <button class="action-share" data-title="' + img.title + '" data-id="' + img.id + '">Share</button>';
html += ' <button class="action-copy" data-id="' + img.id + '">Copy Link</button>';
html += ' </div>';
html += '</div>';
});
gallery.innerHTML = html;
setupLazyLoading();
setupActions();
}
// ---- IntersectionObserver: Lazy Loading ----
function setupLazyLoading() {
var images = document.querySelectorAll('.gallery-item img[data-src]');
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
var img = entry.target;
var placeholder = img.previousElementSibling;
img.onload = function() {
img.classList.add('loaded');
if (placeholder && placeholder.classList.contains('placeholder')) {
placeholder.style.display = 'none';
}
};
img.onerror = function() {
if (placeholder) {
placeholder.textContent = 'Failed to load';
}
};
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
}, {
rootMargin: '150px 0px',
threshold: 0.01
});
images.forEach(function(img) {
observer.observe(img);
});
}
// ---- Fullscreen API ----
function openViewer(src) {
var viewer = document.getElementById('viewer');
var viewerImg = document.getElementById('viewer-img');
viewerImg.src = src;
viewer.classList.add('active');
if (viewer.requestFullscreen) {
viewer.requestFullscreen();
} else if (viewer.webkitRequestFullscreen) {
viewer.webkitRequestFullscreen();
}
}
function closeViewer() {
var viewer = document.getElementById('viewer');
viewer.classList.remove('active');
if (document.fullscreenElement) {
document.exitFullscreen();
} else if (document.webkitFullscreenElement) {
document.webkitExitFullscreen();
}
}
document.addEventListener('fullscreenchange', function() {
if (!document.fullscreenElement) {
document.getElementById('viewer').classList.remove('active');
}
});
// ---- Web Share API ----
function shareImage(title, imageId) {
var url = window.location.origin + '/gallery/' + imageId;
if (navigator.share) {
navigator.share({
title: title,
text: 'Check out this image: ' + title,
url: url
}).catch(function(err) {
if (err.name !== 'AbortError') {
showToast('Share failed');
}
});
} else {
navigator.clipboard.writeText(url).then(function() {
showToast('Link copied (sharing not supported on this browser)');
});
}
}
// ---- Clipboard API ----
function copyImageLink(imageId) {
var url = window.location.origin + '/gallery/' + imageId;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(function() {
showToast('Link copied to clipboard');
}).catch(function() {
showToast('Failed to copy link');
});
} else {
var textarea = document.createElement('textarea');
textarea.value = url;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
showToast('Link copied to clipboard');
}
}
// ---- Action Buttons ----
function setupActions() {
document.querySelectorAll('.action-view').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
openViewer(btn.dataset.src);
});
});
document.querySelectorAll('.action-share').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
shareImage(btn.dataset.title, btn.dataset.id);
});
});
document.querySelectorAll('.action-copy').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
copyImageLink(btn.dataset.id);
});
});
document.getElementById('btn-close-viewer').addEventListener('click', closeViewer);
}
// ---- Page Visibility API: Slideshow Pause/Resume ----
function startSlideshow() {
if (isSlideShowRunning) {
stopSlideshow();
return;
}
isSlideShowRunning = true;
document.getElementById('btn-slideshow').textContent = 'Stop Slideshow';
document.getElementById('slideshow-indicator').classList.add('active');
slideshowInterval = setInterval(function() {
slideshowIndex = (slideshowIndex + 1) % IMAGES.length;
openViewer(IMAGES[slideshowIndex].src);
}, 4000);
}
function stopSlideshow() {
isSlideShowRunning = false;
clearInterval(slideshowInterval);
slideshowInterval = null;
document.getElementById('btn-slideshow').textContent = 'Start Slideshow';
document.getElementById('slideshow-indicator').classList.remove('active');
}
document.addEventListener('visibilitychange', function() {
if (!isSlideShowRunning) return;
if (document.hidden) {
clearInterval(slideshowInterval);
slideshowInterval = null;
document.getElementById('slideshow-indicator').textContent = 'Slideshow paused (tab hidden)';
} else {
document.getElementById('slideshow-indicator').textContent = 'Slideshow running';
slideshowInterval = setInterval(function() {
slideshowIndex = (slideshowIndex + 1) % IMAGES.length;
openViewer(IMAGES[slideshowIndex].src);
}, 4000);
}
});
document.getElementById('btn-slideshow').addEventListener('click', startSlideshow);
// ---- Notification API ----
function enableNotifications() {
if (!('Notification' in window)) {
showToast('Notifications not supported in this browser');
return;
}
if (Notification.permission === 'granted') {
showToast('Notifications already enabled');
simulateNewContent();
return;
}
if (Notification.permission === 'denied') {
showToast('Notifications were previously blocked');
return;
}
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
showToast('Notifications enabled');
simulateNewContent();
}
});
}
function simulateNewContent() {
setTimeout(function() {
if (Notification.permission === 'granted') {
var notification = new Notification('New Photos Available', {
body: '5 new photos have been added to the gallery.',
tag: 'gallery-update',
renotify: true
});
notification.onclick = function() {
window.focus();
notification.close();
showToast('Scroll down to see new content');
};
}
}, 5000);
}
document.getElementById('btn-notify').addEventListener('click', enableNotifications);
// ---- Fullscreen Gallery Button ----
document.getElementById('btn-fullscreen').addEventListener('click', function() {
var gallery = document.getElementById('gallery');
if (gallery.requestFullscreen) {
gallery.requestFullscreen();
} else if (gallery.webkitRequestFullscreen) {
gallery.webkitRequestFullscreen();
}
});
// ---- Initialize ----
renderGallery();
})();
</script>
</body>
</html>
Save this as a standalone HTML file and open it in a browser to see all six APIs working together. The lazy loading fires as you scroll, the viewer enters fullscreen, sharing uses the native dialog on supported platforms, and the slideshow pauses when you switch tabs.
Common Issues and Troubleshooting
1. Clipboard API fails with "Document is not focused"
The Clipboard API requires the document to be focused. If you are calling writeText from a setTimeout or an async callback that fires after the user gesture, the browser may reject it. Always initiate clipboard operations directly from a click handler, or chain .then() calls that stay within the same user gesture context.
2. Notifications show "permission denied" with no prompt
Browsers only show the permission prompt once per origin. If the user previously denied permission, Notification.requestPermission() will resolve to 'denied' without showing a prompt. You cannot programmatically reset this. Guide users to their browser settings to re-enable notifications for your site.
3. IntersectionObserver callback fires immediately
When you call observer.observe(element), the callback fires once with the element's current intersection state. This is by design. If you only want to act on elements entering the viewport, check entry.isIntersecting before executing your logic.
4. Web Share API throws "NotAllowedError"
The Web Share API must be triggered by a user gesture (click, tap, keypress). Calling navigator.share() from a timer, promise chain, or page load will fail. Ensure the share call is in the direct call stack of an event handler.
5. AbortController signal already aborted
Each AbortController can only be aborted once. If you reuse a controller after calling abort(), all new operations attached to its signal will immediately abort. Create a new AbortController instance for each abortable operation or batch.
6. Fullscreen request denied without error
Fullscreen requests must originate from a user gesture and the element must be in the DOM. Some browsers also require the allowfullscreen attribute on iframes. Check document.fullscreenEnabled before attempting fullscreen.
Best Practices
Always feature-detect before using any browser API. Do not rely on user agent strings. Check for the existence of the API object or method directly. Wrap checks in a support object like the
BrowserSupportutility shown above.Provide fallbacks, not errors. When an API is unavailable, degrade gracefully. If Web Share is missing, copy to clipboard instead. If notifications are blocked, show in-page toasts. Users should never see a broken experience because of a missing API.
Respect user gestures. Many APIs (Clipboard, Share, Fullscreen, Notifications) require a user-initiated event. Structure your code so API calls happen synchronously within event handlers, not in deferred callbacks.
Clean up observers and channels. Call
observer.disconnect()when you are done observing. Callchannel.close()when the component unmounts. Leaking observers is a real source of memory issues in single-page applications.Use Page Visibility to save resources. If your app polls a server, animates elements, or plays media, pause those activities when the tab is hidden. This is respectful to users' battery and bandwidth.
Batch Performance API measurements. Do not send a beacon for every mark and measure. Collect metrics and send them in batches, or use
navigator.sendBeacon()on page unload to transmit the final set of measurements.Test on real devices. Many of these APIs behave differently on mobile versus desktop. The Vibration API does nothing on desktop. Geolocation accuracy varies wildly. Web Share opens different dialogs on iOS versus Android. Emulators do not catch these differences.
Do not ask for permissions on page load. Notification and Geolocation permission requests should be contextual. Show a custom UI that explains why you need the permission, then trigger the browser prompt when the user opts in. Permission prompts on page load get denied reflexively.
References
- MDN Web Docs: Web APIs
- MDN: Intersection Observer API
- MDN: Clipboard API
- MDN: Notifications API
- MDN: Page Visibility API
- MDN: Web Share API
- MDN: Performance API
- MDN: Broadcast Channel API
- MDN: AbortController
- MDN: Fullscreen API
- MDN: Web Animations API
- Can I Use — Browser compatibility tables for all APIs covered