Geolocation & Browser APIs
Introduction to Browser APIs
Modern web browsers provide a rich set of built-in APIs that give JavaScript access to device capabilities, browser features, and system information. These APIs allow you to determine a user's geographic location, interact with the browser's history, copy text to the clipboard, share content through native share dialogs, detect whether a page is visible, and much more. Understanding these APIs is essential for building modern web applications that feel native and responsive.
In this lesson, you will learn the most important browser APIs that every JavaScript developer should know. We start with the Geolocation API for determining user location, then explore the Navigator API for browser information, the History API for client-side routing, the Clipboard API for copy and paste operations, the Web Share API for native sharing, the Fullscreen API for immersive experiences, and the Page Visibility API for detecting when users switch tabs. Each API is covered with practical examples and real-world use cases.
The Geolocation API
The Geolocation API provides access to the device's geographic location. It uses various sources to determine position, including GPS, Wi-Fi networks, cell tower triangulation, and IP address geolocation. The API is accessed through the navigator.geolocation object and provides three main methods: getCurrentPosition() for a single location request, watchPosition() for continuous tracking, and clearWatch() to stop tracking.
getCurrentPosition()
The getCurrentPosition() method requests the device's current position. It takes up to three arguments: a success callback, an optional error callback, and an optional options object. The success callback receives a Position object containing the coordinates and a timestamp.
Example: Getting the Current Position
// Basic usage
navigator.geolocation.getCurrentPosition(
(position) => {
console.log('Latitude:', position.coords.latitude);
console.log('Longitude:', position.coords.longitude);
console.log('Accuracy:', position.coords.accuracy, 'meters');
console.log('Timestamp:', new Date(position.timestamp));
},
(error) => {
console.error('Geolocation error:', error.message);
}
);
// With options
navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude, accuracy } = position.coords;
console.log(`Location: ${latitude}, ${longitude}`);
console.log(`Accurate to ${accuracy} meters`);
},
(error) => {
handleGeolocationError(error);
},
{
enableHighAccuracy: true, // Use GPS if available
timeout: 10000, // Wait up to 10 seconds
maximumAge: 60000 // Accept cached position up to 1 minute old
}
);
The Position Object
The Position object passed to the success callback contains a coords property with detailed location information and a timestamp property. The coords object includes the following properties:
- latitude -- The latitude in decimal degrees (always available).
- longitude -- The longitude in decimal degrees (always available).
- accuracy -- The accuracy of the position in meters (always available).
- altitude -- The altitude in meters above sea level (may be null).
- altitudeAccuracy -- The accuracy of the altitude in meters (may be null).
- heading -- The direction of travel in degrees clockwise from true north (may be null).
- speed -- The speed in meters per second (may be null).
Example: Accessing All Position Properties
navigator.geolocation.getCurrentPosition((position) => {
const coords = position.coords;
// Always available
console.log('Latitude:', coords.latitude);
console.log('Longitude:', coords.longitude);
console.log('Accuracy:', coords.accuracy, 'meters');
// May be null depending on device capabilities
if (coords.altitude !== null) {
console.log('Altitude:', coords.altitude, 'meters');
console.log('Altitude accuracy:', coords.altitudeAccuracy, 'meters');
}
if (coords.heading !== null) {
console.log('Heading:', coords.heading, 'degrees');
}
if (coords.speed !== null) {
console.log('Speed:', coords.speed, 'm/s');
console.log('Speed:', (coords.speed * 3.6).toFixed(1), 'km/h');
}
console.log('Timestamp:', new Date(position.timestamp).toLocaleString());
});
Error Handling
The error callback receives a GeolocationPositionError object with a code property and a message property. There are three possible error codes that you should handle:
Example: Comprehensive Error Handling
function handleGeolocationError(error) {
switch (error.code) {
case error.PERMISSION_DENIED:
// Code 1: User denied the permission request
console.error('Location access was denied by the user.');
showMessage('Please enable location access in your browser settings.');
break;
case error.POSITION_UNAVAILABLE:
// Code 2: Location information is unavailable
console.error('Location information is not available.');
showMessage('Unable to determine your location. Please try again.');
break;
case error.TIMEOUT:
// Code 3: The request timed out
console.error('Location request timed out.');
showMessage('Location request took too long. Please try again.');
break;
default:
console.error('An unknown geolocation error occurred.');
showMessage('An unexpected error occurred.');
break;
}
}
// Check if geolocation is supported before using it
function getLocation() {
if (!'geolocation' in navigator) {
showMessage('Geolocation is not supported by your browser.');
return;
}
navigator.geolocation.getCurrentPosition(
onSuccess,
handleGeolocationError,
{ enableHighAccuracy: true, timeout: 10000 }
);
}
watchPosition() and clearWatch()
The watchPosition() method continuously monitors the device's position and calls the success callback whenever the position changes. It returns a watch ID that you can pass to clearWatch() to stop tracking. This is essential for navigation apps, fitness trackers, and any application that needs to follow the user's movement in real time.
Example: Continuous Position Tracking
let watchId = null;
const positions = [];
function startTracking() {
if (watchId !== null) {
console.log('Already tracking.');
return;
}
watchId = navigator.geolocation.watchPosition(
(position) => {
const { latitude, longitude, accuracy, speed } = position.coords;
positions.push({
lat: latitude,
lng: longitude,
accuracy: accuracy,
speed: speed,
time: position.timestamp
});
console.log(`Position update #${positions.length}`);
console.log(` Location: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}`);
console.log(` Accuracy: ${accuracy.toFixed(0)}m`);
if (speed !== null) {
console.log(` Speed: ${(speed * 3.6).toFixed(1)} km/h`);
}
// Update UI with new position
updateMap(latitude, longitude);
updateDistance();
},
(error) => {
handleGeolocationError(error);
},
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 0 // Always get fresh position
}
);
console.log('Tracking started. Watch ID:', watchId);
}
function stopTracking() {
if (watchId !== null) {
navigator.geolocation.clearWatch(watchId);
watchId = null;
console.log('Tracking stopped.');
console.log(`Total positions recorded: ${positions.length}`);
}
}
function updateDistance() {
if (positions.length < 2) return;
let totalDistance = 0;
for (let i = 1; i < positions.length; i++) {
totalDistance += calculateDistance(
positions[i - 1].lat, positions[i - 1].lng,
positions[i].lat, positions[i].lng
);
}
console.log(`Total distance: ${totalDistance.toFixed(2)} km`);
}
// Haversine formula for distance between two coordinates
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Earth radius in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
watchPosition() method fires the callback whenever the device detects a significant position change. The frequency depends on the device, the accuracy setting, and the movement speed. Setting maximumAge: 0 ensures you always get a fresh position rather than a cached one. Keep in mind that continuous GPS tracking drains battery significantly on mobile devices.The Permission Model
Geolocation access follows a strict permission model. The first time your code requests location access, the browser shows a permission prompt. The user can allow, deny, or dismiss the prompt. You can use the Permissions API to check the current permission state without triggering a prompt:
Example: Checking Geolocation Permission
async function checkLocationPermission() {
try {
const result = await navigator.permissions.query({ name: 'geolocation' });
console.log('Permission state:', result.state);
switch (result.state) {
case 'granted':
// Permission already granted, proceed directly
navigator.geolocation.getCurrentPosition(onSuccess);
break;
case 'prompt':
// Permission not yet requested, show explanation first
showExplanation('We need your location to show nearby results.');
break;
case 'denied':
// Permission denied, show instructions to enable
showMessage('Location access is blocked. Please update your browser settings.');
break;
}
// Listen for permission changes
result.addEventListener('change', () => {
console.log('Permission changed to:', result.state);
});
} catch (error) {
console.error('Permissions API not supported:', error);
}
}
The Navigator API
The navigator object provides information about the browser and the device. It exposes properties and methods for detecting browser capabilities, language preferences, network status, and more. While some properties are available for historical reasons, several modern additions are extremely useful for building responsive web applications.
Key Navigator Properties
Example: Useful Navigator Properties
// User agent string (use with caution for detection)
console.log('User Agent:', navigator.userAgent);
// Preferred language
console.log('Language:', navigator.language);
console.log('All languages:', navigator.languages);
// Online status
console.log('Online:', navigator.onLine);
// Number of CPU cores
console.log('CPU cores:', navigator.hardwareConcurrency);
// Device memory (approximate, in GB)
console.log('Device memory:', navigator.deviceMemory, 'GB');
// Max touch points (0 means no touch support)
console.log('Touch points:', navigator.maxTouchPoints);
// PDF viewer support
console.log('PDF viewer:', navigator.pdfViewerEnabled);
// Listen for online/offline events
window.addEventListener('online', () => {
console.log('Back online!');
showNotification('Connection restored.');
});
window.addEventListener('offline', () => {
console.log('Gone offline.');
showNotification('You are offline. Changes will sync when reconnected.');
});
// Practical: Adapt to network status
function fetchData(url) {
if (!navigator.onLine) {
return loadFromCache(url);
}
return fetch(url)
.then(response => response.json())
.catch(() => loadFromCache(url));
}
navigator.userAgent for feature detection. User agent strings are unreliable and can be spoofed. Instead, use feature detection by checking if specific APIs or properties exist. For example, use 'geolocation' in navigator instead of parsing the user agent string to check for mobile browsers.The Screen API
The screen object provides information about the user's display. It includes the screen dimensions, available space, color depth, and pixel density. This information is useful for adapting content to different display capabilities and for positioning windows and dialogs appropriately.
Example: Screen Information
// Full screen dimensions
console.log('Screen width:', screen.width);
console.log('Screen height:', screen.height);
// Available space (minus taskbar, dock, etc.)
console.log('Available width:', screen.availWidth);
console.log('Available height:', screen.availHeight);
// Color information
console.log('Color depth:', screen.colorDepth, 'bits');
console.log('Pixel depth:', screen.pixelDepth, 'bits');
// Device pixel ratio (for high-DPI displays)
console.log('Pixel ratio:', window.devicePixelRatio);
// Screen orientation
console.log('Orientation:', screen.orientation.type);
console.log('Angle:', screen.orientation.angle);
// Listen for orientation changes
screen.orientation.addEventListener('change', () => {
console.log('New orientation:', screen.orientation.type);
console.log('New angle:', screen.orientation.angle);
if (screen.orientation.type.startsWith('landscape')) {
enableLandscapeMode();
} else {
enablePortraitMode();
}
});
// Practical: Load appropriate image resolution
function getOptimalImageSize() {
const dpr = window.devicePixelRatio || 1;
const screenWidth = screen.width * dpr;
if (screenWidth > 2560) return '4k';
if (screenWidth > 1920) return '2k';
if (screenWidth > 1280) return 'hd';
return 'sd';
}
The History API
The History API allows JavaScript to manipulate the browser's session history. It is the foundation of client-side routing in single-page applications (SPAs). Instead of loading new pages from the server, SPAs use the History API to update the URL and manage navigation state while keeping the application running in the same page. The two key methods are pushState() and replaceState(), and the key event is popstate.
pushState() and replaceState()
The pushState() method adds a new entry to the browser's history stack. The replaceState() method modifies the current history entry without adding a new one. Both methods take three arguments: a state object, a title (currently ignored by most browsers), and an optional URL.
Example: Using pushState and replaceState
// pushState adds a new history entry
// Syntax: history.pushState(state, title, url)
// Navigate to a new "page" without reloading
history.pushState(
{ page: 'about', section: 'team' }, // State object
'', // Title (ignored)
'/about/team' // New URL
);
// The URL bar now shows "/about/team" but no page load occurred
console.log('Current URL:', window.location.pathname);
// replaceState modifies the current entry
// Useful for updating state without creating history entries
history.replaceState(
{ page: 'about', section: 'team', scrollY: 500 },
'',
'/about/team'
);
// Access the current state
console.log('Current state:', history.state);
// Navigate back and forward programmatically
history.back(); // Same as clicking the Back button
history.forward(); // Same as clicking the Forward button
history.go(-2); // Go back two entries
history.go(1); // Go forward one entry
// Check history length
console.log('History length:', history.length);
The popstate Event
The popstate event fires when the user navigates through the history (by clicking the Back or Forward buttons, or by calling history.back(), history.forward(), or history.go()). The event object contains the state that was passed to pushState() or replaceState(). Note that popstate does not fire when pushState() or replaceState() is called -- it only fires during actual navigation.
Example: Simple Client-Side Router
// Simple client-side router using the History API
class Router {
constructor() {
this.routes = {};
window.addEventListener('popstate', (e) => {
this.handleRoute(window.location.pathname, e.state);
});
}
addRoute(path, handler) {
this.routes[path] = handler;
}
navigate(path, state = {}) {
history.pushState(state, '', path);
this.handleRoute(path, state);
}
handleRoute(path, state) {
// Find matching route
const handler = this.routes[path];
if (handler) {
handler(state);
} else {
// Check for dynamic routes
for (const [pattern, routeHandler] of Object.entries(this.routes)) {
const regex = new RegExp('^/' + pattern.replace(/:\w+/g, '([^/]+)') + '$');
const match = path.match(regex);
if (match) {
routeHandler(state, ...match.slice(1));
return;
}
}
this.handle404();
}
}
handle404() {
document.getElementById('app').innerHTML = '<h1>Page Not Found</h1>';
}
}
// Usage
const router = new Router();
router.addRoute('/', () => {
document.getElementById('app').innerHTML = '<h1>Home Page</h1>';
});
router.addRoute('/about', () => {
document.getElementById('app').innerHTML = '<h1>About Page</h1>';
});
router.addRoute('/products/:id', (state, id) => {
document.getElementById('app').innerHTML = `<h1>Product ${id}</h1>`;
});
// Intercept link clicks for SPA navigation
document.addEventListener('click', (e) => {
const link = e.target.closest('a[data-route]');
if (link) {
e.preventDefault();
router.navigate(link.getAttribute('href'));
}
});
pushState(), the state object is serialized using the structured clone algorithm. This means you can store objects, arrays, dates, and other structured data, but you cannot store functions, DOM elements, or class instances. Keep the state object small and serializable.The URL API
The URL API provides a clean, object-oriented way to parse, construct, and manipulate URLs. The URL constructor creates URL objects from strings, and the URLSearchParams interface handles query string parameters. This API eliminates the need for manual string parsing and regex-based URL manipulation.
Example: Working with URLs
// Creating and parsing URLs
const url = new URL('https://example.com:8080/products?category=shoes&sort=price#reviews');
console.log('Protocol:', url.protocol); // "https:"
console.log('Host:', url.host); // "example.com:8080"
console.log('Hostname:', url.hostname); // "example.com"
console.log('Port:', url.port); // "8080"
console.log('Pathname:', url.pathname); // "/products"
console.log('Search:', url.search); // "?category=shoes&sort=price"
console.log('Hash:', url.hash); // "#reviews"
console.log('Origin:', url.origin); // "https://example.com:8080"
// Working with search parameters
const params = url.searchParams;
console.log('Category:', params.get('category')); // "shoes"
console.log('Sort:', params.get('sort')); // "price"
console.log('Has color:', params.has('color')); // false
// Modifying parameters
params.set('page', '2');
params.append('color', 'red');
params.append('color', 'blue');
params.delete('sort');
console.log('Updated URL:', url.toString());
// "https://example.com:8080/products?category=shoes&page=2&color=red&color=blue#reviews"
// Iterating over parameters
for (const [key, value] of params) {
console.log(`${key} = ${value}`);
}
// Get all values for a parameter
console.log('Colors:', params.getAll('color')); // ["red", "blue"]
// Building URLs from the current page
const apiUrl = new URL('/api/data', window.location.origin);
apiUrl.searchParams.set('format', 'json');
console.log('API URL:', apiUrl.toString());
// Relative URL resolution
const base = new URL('https://example.com/docs/guide/');
const relative = new URL('../api/reference', base);
console.log('Resolved:', relative.toString());
// "https://example.com/docs/api/reference"
The Clipboard API
The Clipboard API provides asynchronous methods to read from and write to the system clipboard. It replaces the older document.execCommand('copy') approach with a modern, promise-based API. The two primary methods are navigator.clipboard.writeText() for copying text and navigator.clipboard.readText() for pasting text.
Example: Copy and Paste with the Clipboard API
// Copy text to clipboard
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log('Text copied to clipboard!');
showNotification('Copied!');
} catch (error) {
console.error('Failed to copy:', error);
// Fallback for older browsers
fallbackCopy(text);
}
}
// Read text from clipboard
async function pasteFromClipboard() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted text:', text);
return text;
} catch (error) {
console.error('Failed to read clipboard:', error);
return null;
}
}
// Fallback copy method for older browsers
function fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
showNotification('Copied!');
} catch (err) {
console.error('Fallback copy failed:', err);
}
document.body.removeChild(textarea);
}
// Practical: Copy button for code blocks
document.querySelectorAll('.code-block').forEach(block => {
const copyBtn = document.createElement('button');
copyBtn.textContent = 'Copy';
copyBtn.className = 'copy-button';
copyBtn.addEventListener('click', async () => {
const code = block.querySelector('code').textContent;
await copyToClipboard(code);
copyBtn.textContent = 'Copied!';
setTimeout(() => {
copyBtn.textContent = 'Copy';
}, 2000);
});
block.style.position = 'relative';
block.appendChild(copyBtn);
});
// Copy rich content (HTML, images)
async function copyRichContent() {
try {
const blob = new Blob(['<b>Bold text</b>'], { type: 'text/html' });
const clipboardItem = new ClipboardItem({
'text/html': blob,
'text/plain': new Blob(['Bold text'], { type: 'text/plain' })
});
await navigator.clipboard.write([clipboardItem]);
console.log('Rich content copied!');
} catch (error) {
console.error('Failed to copy rich content:', error);
}
}
readText) requires explicit user permission through a browser prompt. Writing to the clipboard (writeText) is allowed in response to user gestures (like click events) without a prompt in most browsers. Never read the clipboard without a clear user-initiated action.The Web Share API
The Web Share API invokes the native sharing mechanism of the operating system, allowing users to share content through installed apps like messaging, email, social media, and more. This provides a consistent, familiar sharing experience that uses the apps the user already has installed on their device.
Example: Using the Web Share API
// Check if Web Share is supported
function isShareSupported() {
return 'share' in navigator;
}
// Basic text sharing
async function shareContent() {
if (!isShareSupported()) {
// Fallback: Show custom share dialog or copy link
showCustomShareDialog();
return;
}
try {
await navigator.share({
title: 'Check out this article',
text: 'I found this really interesting article about JavaScript.',
url: window.location.href
});
console.log('Content shared successfully!');
} catch (error) {
if (error.name === 'AbortError') {
console.log('User cancelled sharing.');
} else {
console.error('Error sharing:', error);
}
}
}
// Share files (images, documents)
async function shareFile(file) {
if (!navigator.canShare || !navigator.canShare({ files: [file] })) {
console.log('File sharing not supported.');
return;
}
try {
await navigator.share({
title: 'Shared file',
files: [file]
});
console.log('File shared successfully!');
} catch (error) {
console.error('Error sharing file:', error);
}
}
// Practical: Share button component
function createShareButton(container, data) {
const button = document.createElement('button');
button.textContent = 'Share';
button.className = 'share-button';
if (isShareSupported()) {
button.addEventListener('click', async () => {
try {
await navigator.share(data);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Share failed:', error);
}
}
});
} else {
// Fallback: Copy URL to clipboard
button.addEventListener('click', async () => {
await navigator.clipboard.writeText(data.url || window.location.href);
button.textContent = 'Link copied!';
setTimeout(() => { button.textContent = 'Share'; }, 2000);
});
}
container.appendChild(button);
}
// Usage
createShareButton(document.getElementById('article'), {
title: document.title,
text: 'Check out this page!',
url: window.location.href
});
'share' in navigator and provide a fallback for unsupported browsers. Common fallbacks include copying the URL to the clipboard, showing a custom share dialog with links to social media platforms, or opening a mailto link.The Fullscreen API
The Fullscreen API allows you to present any element in fullscreen mode, taking over the entire display. This is commonly used for video players, image galleries, presentations, games, and immersive reading experiences. The API provides methods to enter and exit fullscreen mode, properties to check the current state, and events to react to changes.
Example: Working with the Fullscreen API
// Enter fullscreen
async function enterFullscreen(element) {
try {
if (element.requestFullscreen) {
await element.requestFullscreen();
} else if (element.webkitRequestFullscreen) {
await element.webkitRequestFullscreen(); // Safari
}
console.log('Entered fullscreen mode.');
} catch (error) {
console.error('Failed to enter fullscreen:', error);
}
}
// Exit fullscreen
async function exitFullscreen() {
try {
if (document.exitFullscreen) {
await document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
await document.webkitExitFullscreen(); // Safari
}
} catch (error) {
console.error('Failed to exit fullscreen:', error);
}
}
// Toggle fullscreen
async function toggleFullscreen(element) {
if (document.fullscreenElement) {
await exitFullscreen();
} else {
await enterFullscreen(element);
}
}
// Check fullscreen state
function isFullscreen() {
return !!document.fullscreenElement;
}
// Listen for fullscreen changes
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
console.log('Element in fullscreen:', document.fullscreenElement.id);
} else {
console.log('Exited fullscreen.');
}
});
// Practical: Fullscreen video player
const videoContainer = document.getElementById('video-container');
const fullscreenBtn = document.getElementById('fullscreen-btn');
fullscreenBtn.addEventListener('click', () => {
toggleFullscreen(videoContainer);
});
// Style fullscreen elements with CSS
// Use the :fullscreen pseudo-class
// :fullscreen { background: black; }
// ::backdrop { background: rgba(0, 0, 0, 0.9); }
// Practical: Fullscreen image gallery
class FullscreenGallery {
constructor(container) {
this.container = container;
this.images = container.querySelectorAll('img');
this.currentIndex = 0;
this.images.forEach((img, index) => {
img.addEventListener('click', () => {
this.currentIndex = index;
this.openFullscreen();
});
});
document.addEventListener('keydown', (e) => {
if (!isFullscreen()) return;
if (e.key === 'ArrowRight') this.next();
if (e.key === 'ArrowLeft') this.previous();
if (e.key === 'Escape') exitFullscreen();
});
}
async openFullscreen() {
await enterFullscreen(this.container);
this.showImage(this.currentIndex);
}
next() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
this.showImage(this.currentIndex);
}
previous() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
this.showImage(this.currentIndex);
}
showImage(index) {
this.images.forEach((img, i) => {
img.style.display = i === index ? 'block' : 'none';
});
}
}
The Page Visibility API
The Page Visibility API lets you detect when a page becomes visible or hidden. A page is considered hidden when the user switches to another tab, minimizes the browser window, or locks the screen. This API is invaluable for optimizing performance and user experience -- you can pause animations, stop polling, mute audio, and save resources when the user is not actively viewing your page.
Example: Using the Page Visibility API
// Check current visibility state
console.log('Visibility state:', document.visibilityState);
// Values: "visible", "hidden"
console.log('Is hidden:', document.hidden);
// Boolean: true or false
// Listen for visibility changes
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('Page is now hidden.');
onPageHidden();
} else {
console.log('Page is now visible.');
onPageVisible();
}
});
// Practical: Pause and resume operations
let animationId = null;
let pollingInterval = null;
function onPageVisible() {
// Resume animation
if (!animationId) {
animationId = requestAnimationFrame(animate);
}
// Resume data polling
if (!pollingInterval) {
pollingInterval = setInterval(fetchUpdates, 30000);
fetchUpdates(); // Fetch immediately on return
}
// Resume video playback
const video = document.querySelector('video');
if (video && video.dataset.wasPlaying === 'true') {
video.play();
}
}
function onPageHidden() {
// Pause animation
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
// Stop data polling
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
}
// Pause video playback
const video = document.querySelector('video');
if (video && !video.paused) {
video.dataset.wasPlaying = 'true';
video.pause();
}
// Save draft or progress
saveDraft();
}
// Practical: Track actual time spent on page
class TimeTracker {
constructor() {
this.totalTime = 0;
this.startTime = null;
this.isTracking = false;
if (!document.hidden) {
this.start();
}
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pause();
} else {
this.start();
}
});
// Save time before page unload
window.addEventListener('beforeunload', () => {
this.pause();
this.save();
});
}
start() {
if (!this.isTracking) {
this.startTime = Date.now();
this.isTracking = true;
}
}
pause() {
if (this.isTracking && this.startTime) {
this.totalTime += Date.now() - this.startTime;
this.startTime = null;
this.isTracking = false;
}
}
getTimeSpent() {
let time = this.totalTime;
if (this.isTracking && this.startTime) {
time += Date.now() - this.startTime;
}
return Math.round(time / 1000); // Return seconds
}
save() {
const seconds = this.getTimeSpent();
console.log(`Time on page: ${seconds} seconds`);
// Send to analytics
}
}
const tracker = new TimeTracker();
window.onblur and window.onfocus for detecting when the user is actually viewing your page. The blur and focus events fire when switching between windows or interacting with browser chrome, which may not accurately reflect whether the page content is visible. The visibilitychange event specifically tracks whether the page tab is in the foreground.Real-World Application: Combining Multiple APIs
In real applications, you often combine multiple browser APIs to create cohesive features. Here is an example that combines the Geolocation API, Clipboard API, Web Share API, and Page Visibility API into a location-sharing feature:
Example: Location Sharing Application
class LocationSharer {
constructor() {
this.watchId = null;
this.lastPosition = null;
this.isSharing = false;
// Pause tracking when page is hidden
document.addEventListener('visibilitychange', () => {
if (document.hidden && this.isSharing) {
this.pauseTracking();
} else if (!document.hidden && this.isSharing) {
this.resumeTracking();
}
});
}
async startSharing() {
if (!'geolocation' in navigator) {
alert('Geolocation is not supported.');
return;
}
this.isSharing = true;
this.resumeTracking();
}
resumeTracking() {
if (this.watchId !== null) return;
this.watchId = navigator.geolocation.watchPosition(
(position) => {
this.lastPosition = position;
this.updateUI(position);
},
(error) => {
console.error('Location error:', error.message);
},
{ enableHighAccuracy: true, maximumAge: 5000 }
);
}
pauseTracking() {
if (this.watchId !== null) {
navigator.geolocation.clearWatch(this.watchId);
this.watchId = null;
}
}
stopSharing() {
this.pauseTracking();
this.isSharing = false;
this.lastPosition = null;
}
getShareableLink() {
if (!this.lastPosition) return null;
const { latitude, longitude } = this.lastPosition.coords;
return `https://maps.google.com/?q=${latitude},${longitude}`;
}
async copyLocation() {
const link = this.getShareableLink();
if (!link) {
alert('No location available yet.');
return;
}
try {
await navigator.clipboard.writeText(link);
console.log('Location link copied!');
} catch (error) {
console.error('Copy failed:', error);
}
}
async shareLocation() {
const link = this.getShareableLink();
if (!link) {
alert('No location available yet.');
return;
}
if ('share' in navigator) {
try {
await navigator.share({
title: 'My Location',
text: 'Here is my current location.',
url: link
});
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Share failed:', error);
}
}
} else {
await this.copyLocation();
}
}
updateUI(position) {
const { latitude, longitude, accuracy } = position.coords;
const display = document.getElementById('location-display');
if (display) {
display.textContent = `${latitude.toFixed(6)}, ${longitude.toFixed(6)} (within ${accuracy.toFixed(0)}m)`;
}
}
}
// Initialize
const sharer = new LocationSharer();
document.getElementById('start-btn')?.addEventListener('click', () => {
sharer.startSharing();
});
document.getElementById('share-btn')?.addEventListener('click', () => {
sharer.shareLocation();
});
document.getElementById('copy-btn')?.addEventListener('click', () => {
sharer.copyLocation();
});
document.getElementById('stop-btn')?.addEventListener('click', () => {
sharer.stopSharing();
});
Browser API Feature Detection
Since not all browsers support every API, feature detection is essential. Always check for API availability before using it, and provide meaningful fallbacks for unsupported browsers. Here is a comprehensive feature detection utility:
Example: Feature Detection Utility
const BrowserFeatures = {
geolocation: 'geolocation' in navigator,
clipboard: 'clipboard' in navigator,
share: 'share' in navigator,
fullscreen: 'fullscreenEnabled' in document,
visibility: 'visibilityState' in document,
permissions: 'permissions' in navigator,
onLine: 'onLine' in navigator,
orientation: 'orientation' in screen,
vibration: 'vibrate' in navigator,
bluetooth: 'bluetooth' in navigator,
usb: 'usb' in navigator,
wakeLock: 'wakeLock' in navigator,
storage: 'storage' in navigator,
// Report all supported features
report() {
console.group('Browser Feature Support');
for (const [feature, supported] of Object.entries(this)) {
if (typeof supported === 'boolean') {
console.log(`${feature}: ${supported ? 'Supported' : 'Not supported'}`);
}
}
console.groupEnd();
},
// Check a feature and run callback or fallback
use(feature, callback, fallback) {
if (this[feature]) {
return callback();
} else if (fallback) {
return fallback();
} else {
console.warn(`Feature "${feature}" is not supported in this browser.`);
}
}
};
// Usage
BrowserFeatures.report();
BrowserFeatures.use(
'share',
() => navigator.share({ title: 'Hello', url: location.href }),
() => navigator.clipboard.writeText(location.href)
);
Best Practices for Browser APIs
Working with browser APIs requires attention to security, performance, and user experience. Here are the key best practices:
- Always check for support before using any API. Use
'featureName' in navigatorortypeof API !== 'undefined'patterns. - Request permissions at the right time. Do not ask for geolocation permission when the page loads. Wait until the user takes an action that requires it, and explain why you need the permission first.
- Handle errors gracefully. Every API call can fail. Network issues, permission denials, and unsupported features should all produce helpful messages, not broken interfaces.
- Provide fallbacks. For every API feature, have a fallback for browsers that do not support it. The Web Share API falls back to clipboard copy; the Fullscreen API falls back to a large modal.
- Respect user privacy. Geolocation data is sensitive. Never store or transmit location data without user consent. Always provide a way to stop tracking.
- Optimize for battery life. Continuous GPS tracking, frequent polling, and running animations on hidden pages all drain battery. Use the Page Visibility API to pause expensive operations when the user is not viewing your page.
- Use HTTPS. Many modern APIs require a secure context. Always serve your application over HTTPS in production.
- Keep state objects small. When using the History API, store only the minimum data needed in state objects. Large state objects consume memory and may fail to serialize.
Practice Exercise
Build a "Location Journal" web application that combines multiple browser APIs. The application should have a "Record Location" button that uses the Geolocation API to capture the user's current position. Each recorded location should display the latitude, longitude, accuracy, and timestamp in a list on the page. Add a "Copy All Locations" button that uses the Clipboard API to copy all recorded locations as formatted text. Add a "Share Journal" button that uses the Web Share API (with a clipboard fallback) to share a summary of recorded locations. Implement a fullscreen mode button for the map view using the Fullscreen API. Use the Page Visibility API to pause GPS tracking when the user switches tabs and resume it when they return. Add an online status indicator using navigator.onLine and the online/offline events that shows whether the user has an internet connection. Use the History API to create two views (list view and map view) with proper URL updates so the browser back button works between views. Use the URL API to parse any location query parameters when the page loads. Test every feature including the error cases: deny geolocation permission, go offline, use an unsupported browser for Web Share, and verify all fallbacks work correctly.