Web Platform APIs
The Web Platform provides a rich set of APIs that give web applications access to device capabilities, system features, and user interactions that were previously only available to native apps. These APIs cover everything from clipboard and notifications to payments and geolocation.
Most of these APIs require explicit user permission, work only over HTTPS, and may not be available in all browsers. Always check for feature support before using them and provide meaningful fallbacks when features are unavailable.
The Clipboard API provides programmatic access to copy, cut, and paste operations. It replaces the deprecated document.execCommand methods with a modern, async API that works with text, HTML, images, and files.
| 1 | // Read text from clipboard |
| 2 | async function readClipboardText() { |
| 3 | try { |
| 4 | const text = await navigator.clipboard.readText(); |
| 5 | return text; |
| 6 | } catch (err) { |
| 7 | if (err.name === 'NotAllowedError') { |
| 8 | console.error('Clipboard access denied by user'); |
| 9 | } |
| 10 | return null; |
| 11 | } |
| 12 | } |
| 13 | |
| 14 | // Write text to clipboard |
| 15 | async function copyToClipboard(text) { |
| 16 | try { |
| 17 | await navigator.clipboard.writeText(text); |
| 18 | showToast('Copied to clipboard'); |
| 19 | } catch (err) { |
| 20 | // Fallback for older browsers |
| 21 | const textarea = document.createElement('textarea'); |
| 22 | textarea.value = text; |
| 23 | textarea.style.position = 'fixed'; |
| 24 | textarea.style.opacity = '0'; |
| 25 | document.body.appendChild(textarea); |
| 26 | textarea.select(); |
| 27 | document.execCommand('copy'); |
| 28 | document.body.removeChild(textarea); |
| 29 | showToast('Copied to clipboard'); |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | // Read rich content (HTML, images, files) |
| 34 | async function readClipboardRich() { |
| 35 | try { |
| 36 | const items = await navigator.clipboard.read(); |
| 37 | for (const item of items) { |
| 38 | // Check for images |
| 39 | if (item.types.includes('image/png')) { |
| 40 | const blob = await item.getType('image/png'); |
| 41 | const img = document.createElement('img'); |
| 42 | img.src = URL.createObjectURL(blob); |
| 43 | document.body.appendChild(img); |
| 44 | } |
| 45 | |
| 46 | // Check for HTML |
| 47 | if (item.types.includes('text/html')) { |
| 48 | const blob = await item.getType('text/html'); |
| 49 | const html = await blob.text(); |
| 50 | console.log('HTML content:', html); |
| 51 | } |
| 52 | |
| 53 | // Check for plain text |
| 54 | if (item.types.includes('text/plain')) { |
| 55 | const blob = await item.getType('text/plain'); |
| 56 | const text = await blob.text(); |
| 57 | console.log('Text content:', text); |
| 58 | } |
| 59 | } |
| 60 | } catch (err) { |
| 61 | console.error('Failed to read clipboard:', err); |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | // Write rich content to clipboard |
| 66 | async function copyRichContent() { |
| 67 | const html = '<strong>Bold text</strong> and <em>italic</em>'; |
| 68 | const text = 'Bold text and italic'; |
| 69 | |
| 70 | try { |
| 71 | await navigator.clipboard.write([ |
| 72 | new ClipboardItem({ |
| 73 | 'text/html': new Blob([html], { type: 'text/html' }), |
| 74 | 'text/plain': new Blob([text], { type: 'text/plain' }), |
| 75 | }), |
| 76 | ]); |
| 77 | console.log('Rich content copied'); |
| 78 | } catch (err) { |
| 79 | console.error('Failed to copy:', err); |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | // Copy image to clipboard |
| 84 | async function copyImage(imageUrl) { |
| 85 | const response = await fetch(imageUrl); |
| 86 | const blob = await response.blob(); |
| 87 | await navigator.clipboard.write([ |
| 88 | new ClipboardItem({ |
| 89 | [blob.type]: blob, |
| 90 | }), |
| 91 | ]); |
| 92 | } |
warning
The Notifications API lets you display system notifications to the user, even when the page is not focused. Notifications can include text, images, actions, and badges. They require explicit user permission.
| 1 | // Request notification permission |
| 2 | async function requestNotificationPermission() { |
| 3 | if (!('Notification' in window)) { |
| 4 | console.log('Notifications not supported'); |
| 5 | return false; |
| 6 | } |
| 7 | |
| 8 | if (Notification.permission === 'granted') return true; |
| 9 | if (Notification.permission === 'denied') return false; |
| 10 | |
| 11 | const permission = await Notification.requestPermission(); |
| 12 | return permission === 'granted'; |
| 13 | } |
| 14 | |
| 15 | // Show a simple notification |
| 16 | function showNotification(title, options = {}) { |
| 17 | if (Notification.permission !== 'granted') return; |
| 18 | |
| 19 | const notification = new Notification(title, { |
| 20 | body: options.body || '', |
| 21 | icon: options.icon || '/icons/icon-192.png', |
| 22 | badge: options.badge || '/icons/badge-72.png', |
| 23 | image: options.image || null, |
| 24 | tag: options.tag || 'default', |
| 25 | renotify: options.renotify || false, |
| 26 | silent: options.silent || false, |
| 27 | vibrate: options.vibrate || [100, 50, 100], |
| 28 | timestamp: options.timestamp || Date.now(), |
| 29 | data: options.data || {}, |
| 30 | actions: options.actions || [], |
| 31 | }); |
| 32 | |
| 33 | notification.onclick = (event) => { |
| 34 | event.preventDefault(); |
| 35 | window.focus(); |
| 36 | notification.close(); |
| 37 | if (options.onClick) options.onClick(event); |
| 38 | }; |
| 39 | |
| 40 | notification.onclose = (event) => { |
| 41 | if (options.onClose) options.onClose(event); |
| 42 | }; |
| 43 | |
| 44 | return notification; |
| 45 | } |
| 46 | |
| 47 | // Usage examples |
| 48 | async function initNotifications() { |
| 49 | const granted = await requestNotificationPermission(); |
| 50 | if (!granted) return; |
| 51 | |
| 52 | // Simple notification |
| 53 | showNotification('Hello!', { body: 'Welcome to our app' }); |
| 54 | |
| 55 | // Notification with action buttons |
| 56 | showNotification('New Message', { |
| 57 | body: 'Alice sent you a message', |
| 58 | tag: 'chat-alice', |
| 59 | actions: [ |
| 60 | { action: 'reply', title: 'Reply' }, |
| 61 | { action: 'dismiss', title: 'Dismiss' }, |
| 62 | ], |
| 63 | onClick: () => openChat('alice'), |
| 64 | }); |
| 65 | |
| 66 | // Update an existing notification by tag |
| 67 | showNotification('Upload Progress', { |
| 68 | body: '45% complete', |
| 69 | tag: 'upload-123', |
| 70 | }); |
| 71 | |
| 72 | // Later, update the same notification |
| 73 | setTimeout(() => { |
| 74 | showNotification('Upload Progress', { |
| 75 | body: '100% complete!', |
| 76 | tag: 'upload-123', |
| 77 | renotify: true, |
| 78 | }); |
| 79 | }, 5000); |
| 80 | } |
| 81 | |
| 82 | // Notification event handling in service worker |
| 83 | self.addEventListener('notificationclick', (event) => { |
| 84 | event.notification.close(); |
| 85 | const action = event.action; |
| 86 | const data = event.notification.data; |
| 87 | |
| 88 | switch (action) { |
| 89 | case 'reply': |
| 90 | openReplyWindow(data.conversationId); |
| 91 | break; |
| 92 | case 'dismiss': |
| 93 | break; |
| 94 | default: |
| 95 | event.waitUntil( |
| 96 | clients.openWindow(data.url || '/') |
| 97 | ); |
| 98 | } |
| 99 | }); |
info
The Payment Request API provides a native browser checkout flow that pre-fills payment information from the user's stored methods. It reduces checkout friction and provides a consistent payment experience across websites.
| 1 | // Check if Payment Request is available |
| 2 | const canMakePayment = async () => { |
| 3 | if (!('PaymentRequest' in window)) return false; |
| 4 | try { |
| 5 | const request = new PaymentRequest( |
| 6 | [{ supportedMethods: 'basic-card' }], |
| 7 | { total: { label: 'Test', amount: { currency: 'USD', value: '0' } } } |
| 8 | ); |
| 9 | return await request.canMakePayment(); |
| 10 | } catch { |
| 11 | return false; |
| 12 | } |
| 13 | }; |
| 14 | |
| 15 | // Create and show a payment request |
| 16 | async function processPayment(cartItems) { |
| 17 | const supportedMethods = [ |
| 18 | { |
| 19 | supportedMethods: 'basic-card', |
| 20 | data: { |
| 21 | supportedNetworks: ['visa', 'mastercard', 'amex'], |
| 22 | supportedTypes: ['credit', 'debit'], |
| 23 | }, |
| 24 | }, |
| 25 | // Google Pay |
| 26 | { |
| 27 | supportedMethods: 'https://google.com/pay', |
| 28 | data: { |
| 29 | environment: 'TEST', |
| 30 | apiVersion: 2, |
| 31 | apiVersionMinor: 0, |
| 32 | merchantInfo: { |
| 33 | merchantId: 'YOUR_MERCHANT_ID', |
| 34 | merchantName: 'Example Store', |
| 35 | }, |
| 36 | allowedPaymentMethods: [{ |
| 37 | type: 'CARD', |
| 38 | parameters: { |
| 39 | allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'], |
| 40 | allowedCardNetworks: ['VISA', 'MASTERCARD'], |
| 41 | }, |
| 42 | tokenizationSpecification: { |
| 43 | type: 'PAYMENT_GATEWAY', |
| 44 | parameters: { gateway: 'example', gatewayMerchantId: 'id' }, |
| 45 | }, |
| 46 | }], |
| 47 | }, |
| 48 | }, |
| 49 | ]; |
| 50 | |
| 51 | const paymentDetails = { |
| 52 | total: { |
| 53 | label: 'Total', |
| 54 | amount: { |
| 55 | currency: 'USD', |
| 56 | value: calculateTotal(cartItems).toFixed(2), |
| 57 | }, |
| 58 | }, |
| 59 | displayItems: cartItems.map((item) => ({ |
| 60 | label: item.name, |
| 61 | amount: { currency: 'USD', value: item.price.toFixed(2) }, |
| 62 | })), |
| 63 | shippingOptions: [ |
| 64 | { |
| 65 | id: 'standard', |
| 66 | label: 'Standard Shipping', |
| 67 | amount: { currency: 'USD', value: '5.99' }, |
| 68 | selected: true, |
| 69 | }, |
| 70 | { |
| 71 | id: 'express', |
| 72 | label: 'Express Shipping', |
| 73 | amount: { currency: 'USD', value: '14.99' }, |
| 74 | }, |
| 75 | ], |
| 76 | }; |
| 77 | |
| 78 | try { |
| 79 | const request = new PaymentRequest(supportedMethods, paymentDetails); |
| 80 | |
| 81 | // Handle shipping option changes |
| 82 | request.addEventListener('shippingoptionchange', (event) => { |
| 83 | const shippingCost = event.target.shippingOption === 'express' |
| 84 | ? '14.99' : '5.99'; |
| 85 | event.updateWith({ |
| 86 | total: { |
| 87 | label: 'Total', |
| 88 | amount: { |
| 89 | currency: 'USD', |
| 90 | value: (calculateTotal(cartItems) + parseFloat(shippingCost)).toFixed(2), |
| 91 | }, |
| 92 | }, |
| 93 | }); |
| 94 | }); |
| 95 | |
| 96 | // Show payment UI |
| 97 | const paymentResponse = await request.show(); |
| 98 | |
| 99 | // Send payment data to your server for processing |
| 100 | const result = await fetch('/api/process-payment', { |
| 101 | method: 'POST', |
| 102 | headers: { 'Content-Type': 'application/json' }, |
| 103 | body: JSON.stringify({ |
| 104 | paymentToken: paymentResponse.details, |
| 105 | method: paymentResponse.methodName, |
| 106 | }), |
| 107 | }); |
| 108 | |
| 109 | if (result.ok) { |
| 110 | await paymentResponse.complete('success'); |
| 111 | showOrderConfirmation(); |
| 112 | } else { |
| 113 | await paymentResponse.complete('fail'); |
| 114 | showPaymentError(); |
| 115 | } |
| 116 | } catch (err) { |
| 117 | if (err.name === 'AbortError') { |
| 118 | console.log('User cancelled payment'); |
| 119 | } else { |
| 120 | console.error('Payment error:', err); |
| 121 | } |
| 122 | } |
| 123 | } |
best practice
The Geolocation API provides access to the user's physical location. It works via GPS, WiFi, cell towers, or IP address depending on the device and available sensors. Always request permission before accessing location data.
| 1 | // Check geolocation support |
| 2 | if (!('geolocation' in navigator)) { |
| 3 | console.error('Geolocation not supported'); |
| 4 | } |
| 5 | |
| 6 | // Get current position |
| 7 | async function getCurrentPosition() { |
| 8 | return new Promise((resolve, reject) => { |
| 9 | navigator.geolocation.getCurrentPosition( |
| 10 | (position) => { |
| 11 | resolve({ |
| 12 | lat: position.coords.latitude, |
| 13 | lng: position.coords.longitude, |
| 14 | accuracy: position.coords.accuracy, // meters |
| 15 | altitude: position.coords.altitude, |
| 16 | altitudeAccuracy: position.coords.altitudeAccuracy, |
| 17 | heading: position.coords.heading, // degrees from north |
| 18 | speed: position.coords.speed, // meters/second |
| 19 | timestamp: position.timestamp, |
| 20 | }); |
| 21 | }, |
| 22 | (error) => { |
| 23 | switch (error.code) { |
| 24 | case error.PERMISSION_DENIED: |
| 25 | reject(new Error('Location permission denied')); |
| 26 | break; |
| 27 | case error.POSITION_UNAVAILABLE: |
| 28 | reject(new Error('Location unavailable')); |
| 29 | break; |
| 30 | case error.TIMEOUT: |
| 31 | reject(new Error('Location request timed out')); |
| 32 | break; |
| 33 | } |
| 34 | }, |
| 35 | { |
| 36 | enableHighAccuracy: true, // Use GPS if available |
| 37 | timeout: 10000, // 10 second timeout |
| 38 | maximumAge: 60000, // Accept cached position up to 1 min old |
| 39 | } |
| 40 | ); |
| 41 | }); |
| 42 | } |
| 43 | |
| 44 | // Watch position changes (like a moving vehicle) |
| 45 | let watchId = null; |
| 46 | |
| 47 | function startTracking() { |
| 48 | watchId = navigator.geolocation.watchPosition( |
| 49 | (position) => { |
| 50 | updateMap({ |
| 51 | lat: position.coords.latitude, |
| 52 | lng: position.coords.longitude, |
| 53 | }); |
| 54 | }, |
| 55 | (error) => console.error('Tracking error:', error), |
| 56 | { enableHighAccuracy: true, maximumAge: 5000 } |
| 57 | ); |
| 58 | } |
| 59 | |
| 60 | function stopTracking() { |
| 61 | if (watchId !== null) { |
| 62 | navigator.geolocation.clearWatch(watchId); |
| 63 | watchId = null; |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | // Calculate distance between two coordinates (Haversine formula) |
| 68 | function haversineDistance(lat1, lon1, lat2, lon2) { |
| 69 | const R = 6371e3; // Earth radius in meters |
| 70 | const toRad = (deg) => (deg * Math.PI) / 180; |
| 71 | |
| 72 | const dLat = toRad(lat2 - lat1); |
| 73 | const dLon = toRad(lon2 - lon1); |
| 74 | const a = |
| 75 | Math.sin(dLat / 2) ** 2 + |
| 76 | Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * |
| 77 | Math.sin(dLon / 2) ** 2; |
| 78 | const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); |
| 79 | |
| 80 | return R * c; // distance in meters |
| 81 | } |
| 82 | |
| 83 | // Geofencing — trigger when entering/leaving an area |
| 84 | class Geofence { |
| 85 | constructor(center, radius) { |
| 86 | this.center = center; |
| 87 | this.radius = radius; |
| 88 | this.isInside = false; |
| 89 | } |
| 90 | |
| 91 | check(lat, lng) { |
| 92 | const distance = haversineDistance( |
| 93 | this.center.lat, this.center.lng, lat, lng |
| 94 | ); |
| 95 | const nowInside = distance <= this.radius; |
| 96 | |
| 97 | if (nowInside && !this.isInside) { |
| 98 | this.onEnter?.(); |
| 99 | } else if (!nowInside && this.isInside) { |
| 100 | this.onLeave?.(); |
| 101 | } |
| 102 | |
| 103 | this.isInside = nowInside; |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | // Usage |
| 108 | const homeGeofence = new Geofence( |
| 109 | { lat: 37.7749, lng: -122.4194 }, |
| 110 | 100 // 100 meter radius |
| 111 | ); |
| 112 | |
| 113 | homeGeofence.onEnter = () => console.log('Welcome home!'); |
| 114 | homeGeofence.onLeave = () => console.log('Leaving home area'); |
| 115 | |
| 116 | startTracking(); |
warning
Device APIs provide access to hardware sensors like accelerometers, gyroscopes, and orientation sensors. They enable motion-based interactions, fitness tracking, and augmented reality experiences.
| 1 | // Device Orientation (accelerometer + gyroscope) |
| 2 | function handleOrientation(event) { |
| 3 | const { alpha, beta, gamma } = event; |
| 4 | // alpha: compass direction (0-360 degrees) |
| 5 | // beta: front-to-back tilt (-180 to 180 degrees) |
| 6 | // gamma: left-to-right tilt (-90 to 90 degrees) |
| 7 | |
| 8 | const compass = alpha !== null ? alpha : 'N/A'; |
| 9 | const tiltForward = beta !== null ? beta.toFixed(1) : 'N/A'; |
| 10 | const tiltSide = gamma !== null ? gamma.toFixed(1) : 'N/A'; |
| 11 | |
| 12 | console.log(`Compass: ${compass}° | Forward: ${tiltForward}° | Side: ${tiltSide}°`); |
| 13 | } |
| 14 | |
| 15 | // Request permission on iOS 13+ |
| 16 | async function enableOrientation() { |
| 17 | if (typeof DeviceOrientationEvent.requestPermission === 'function') { |
| 18 | const permission = await DeviceOrientationEvent.requestPermission(); |
| 19 | if (permission === 'granted') { |
| 20 | window.addEventListener('deviceorientation', handleOrientation); |
| 21 | } |
| 22 | } else { |
| 23 | window.addEventListener('deviceorientation', handleOrientation); |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | // Device Motion (acceleration) |
| 28 | function handleMotion(event) { |
| 29 | const { acceleration, accelerationIncludingGravity, rotationRate } = event; |
| 30 | |
| 31 | console.log('Acceleration:', { |
| 32 | x: acceleration?.x, |
| 33 | y: acceleration?.y, |
| 34 | z: acceleration?.z, |
| 35 | }); |
| 36 | |
| 37 | console.log('Rotation:', { |
| 38 | alpha: rotationRate?.alpha, // compass rate |
| 39 | beta: rotationRate?.beta, // front-back rate |
| 40 | gamma: rotationRate?.gamma, // left-right rate |
| 41 | }); |
| 42 | } |
| 43 | |
| 44 | window.addEventListener('devicemotion', handleMotion); |
| 45 | |
| 46 | // Battery Status API |
| 47 | async function monitorBattery() { |
| 48 | if (!('getBattery' in navigator)) return; |
| 49 | |
| 50 | const battery = await navigator.getBattery(); |
| 51 | |
| 52 | function updateBatteryInfo() { |
| 53 | console.log(`Battery: ${(battery.level * 100).toFixed(0)}%`); |
| 54 | console.log(`Charging: ${battery.charging}`); |
| 55 | console.log(`Time to full: ${battery.chargingTime} seconds`); |
| 56 | console.log(`Time remaining: ${battery.dischargingTime} seconds`); |
| 57 | } |
| 58 | |
| 59 | battery.addEventListener('levelchange', updateBatteryInfo); |
| 60 | battery.addEventListener('chargingchange', updateBatteryInfo); |
| 61 | battery.addEventListener('chargingtimechange', updateBatteryInfo); |
| 62 | battery.addEventListener('dischargingtimechange', updateBatteryInfo); |
| 63 | updateBatteryInfo(); |
| 64 | } |
| 65 | |
| 66 | // Fullscreen API |
| 67 | function toggleFullscreen() { |
| 68 | if (!document.fullscreenElement) { |
| 69 | document.documentElement.requestFullscreen().catch((err) => { |
| 70 | console.error('Fullscreen failed:', err); |
| 71 | }); |
| 72 | } else { |
| 73 | document.exitFullscreen(); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // Vibration API (mobile only) |
| 78 | function vibrate(pattern) { |
| 79 | if ('vibrate' in navigator) { |
| 80 | navigator.vibrate(pattern); // ms, or array of vibrate/pause |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | vibrate(200); // Vibrate 200ms |
| 85 | vibrate([100, 50, 100]); // Vibrate 100ms, pause 50ms, vibrate 100ms |
| 86 | |
| 87 | // Page Visibility API |
| 88 | document.addEventListener('visibilitychange', () => { |
| 89 | if (document.hidden) { |
| 90 | console.log('Page hidden — pause expensive operations'); |
| 91 | pauseAnimations(); |
| 92 | } else { |
| 93 | console.log('Page visible — resume operations'); |
| 94 | resumeAnimations(); |
| 95 | } |
| 96 | }); |
info
Knowing when a page is visible or focused helps optimize resource usage — pause animations, reduce polling, pause video, or save state when the user switches away.
| 1 | // Page Visibility — detect tab visibility |
| 2 | class VisibilityManager { |
| 3 | constructor() { |
| 4 | this.listeners = { visible: [], hidden: [] }; |
| 5 | document.addEventListener('visibilitychange', () => { |
| 6 | const state = document.hidden ? 'hidden' : 'visible'; |
| 7 | this.listeners[state].forEach((cb) => cb()); |
| 8 | }); |
| 9 | } |
| 10 | |
| 11 | onVisible(callback) { |
| 12 | this.listeners.visible.push(callback); |
| 13 | } |
| 14 | |
| 15 | onHidden(callback) { |
| 16 | this.listeners.hidden.push(callback); |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | const visibility = new VisibilityManager(); |
| 21 | visibility.onHidden(() => { |
| 22 | pauseAutoRefresh(); |
| 23 | saveDraft(); |
| 24 | }); |
| 25 | visibility.onVisible(() => { |
| 26 | resumeAutoRefresh(); |
| 27 | refreshData(); |
| 28 | }); |
| 29 | |
| 30 | // Focus/Blur events |
| 31 | window.addEventListener('focus', () => { |
| 32 | console.log('Window focused'); |
| 33 | resumeWebSocket(); |
| 34 | checkForUpdates(); |
| 35 | }); |
| 36 | |
| 37 | window.addEventListener('blur', () => { |
| 38 | console.log('Window blurred'); |
| 39 | pauseWebSocket(); |
| 40 | }); |
| 41 | |
| 42 | // Document.hasFocus() — check current focus state |
| 43 | if (document.hasFocus()) { |
| 44 | startRealTimeUpdates(); |
| 45 | } else { |
| 46 | queueUpdatesForLater(); |
| 47 | } |
| 48 | |
| 49 | // Freezing detection (Page Lifecycle API) |
| 50 | document.addEventListener('freeze', () => { |
| 51 | console.log('Page frozen — release expensive resources'); |
| 52 | cancelTimers(); |
| 53 | closeWebSocket(); |
| 54 | }); |
| 55 | |
| 56 | document.addEventListener('resume', () => { |
| 57 | console.log('Page resumed'); |
| 58 | reconnectWebSocket(); |
| 59 | }); |
| 60 | |
| 61 | // Practical example: pause video when tab is hidden |
| 62 | const video = document.getElementById('main-video'); |
| 63 | visibility.onHidden(() => { |
| 64 | if (!video.paused) video.pause(); |
| 65 | }); |
| 66 | visibility.onVisible(() => { |
| 67 | if (video.dataset.wasPlaying === 'true') video.play(); |
| 68 | }); |
| 69 | video.addEventListener('play', () => { video.dataset.wasPlaying = 'true'; }); |
| 70 | video.addEventListener('pause', () => { video.dataset.wasPlaying = 'false'; }); |
best practice
- Clipboard API replaces execCommand — async, secure, and supports rich content
- Always request notification permission after user gesture with a clear value proposition
- Payment Request API reduces checkout friction — provide a fallback for unsupported browsers
- Geolocation requires HTTPS and user permission — use enableHighAccuracy sparingly
- Device Orientation needs permission on iOS 13+ — feature-detect for desktop
- Page Visibility API saves resources by pausing work on hidden tabs
- Web Share API triggers native share dialogs — provide copy-to-clipboard fallback