The Browser
Is Your
Server
Twenty HTML5 Browser APIs — each a live, fully-working demo. Canvas graphics, Web Audio synthesizer, Speech recognition, native Dialogs, Web Components, WebRTC camera, Service Workers, cross-tab sync, and much more. Zero frameworks, zero build tools.
§ 01 — 2D Rendering Context
Canvas API — Paint App
The <canvas> element gives you a 2D (or WebGL 3D) pixel surface scriptable entirely in JavaScript — charts, games, image processing, drawing apps, animations.
const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); // ── DRAW ────────────────────────────────────── ctx.strokeStyle = '#00c878'; ctx.lineWidth = 4; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.beginPath(); ctx.moveTo(10, 10); ctx.lineTo(100, 100); ctx.stroke(); // ── SHAPES ──────────────────────────────────── ctx.fillStyle = 'rgba(0,200,120,.3)'; ctx.fillRect(20, 20, 80, 50); ctx.strokeRect(20, 20, 80, 50); ctx.beginPath(); ctx.arc(60, 60, 30, 0, Math.PI * 2); ctx.fill(); // ── TEXT ────────────────────────────────────── ctx.font = 'bold 24px Playfair Display'; ctx.fillStyle = '#e8b84b'; ctx.fillText('Hello Canvas!', 10, 50); // ── GRADIENTS ──────────────────────────────── const g = ctx.createLinearGradient(0,0,200,0); g.addColorStop(0, '#00c878'); g.addColorStop(1, '#e8b84b'); ctx.fillStyle = g; // ── EXPORT ──────────────────────────────────── const link = document.createElement('a'); link.download = 'drawing.png'; link.href = canvas.toDataURL('image/png'); link.click();
§ 02 — AudioContext + Oscillator
Web Audio API — Piano
Build a fully functional synthesizer using AudioContext and OscillatorNode — no audio files needed. Add filters, reverb, delay, and a live frequency visualizer.
const ctx = new AudioContext(); function playNote(freq, decayMs = 800) { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; // sine|square|sawtooth|triangle osc.frequency.setValueAtTime(freq, ctx.currentTime); // Envelope — attack + decay gain.gain.setValueAtTime(0, ctx.currentTime); gain.gain.linearRampToValueAtTime(0.8, ctx.currentTime + 0.01); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + decayMs/1000); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + decayMs/1000); } // ── FREQUENCY ANALYSER ──────────────────────── const analyser = ctx.createAnalyser(); analyser.fftSize = 256; const data = new Uint8Array(analyser.frequencyBinCount); function drawViz() { analyser.getByteFrequencyData(data); // draw bars on canvas... requestAnimationFrame(drawViz); } // ── FILTER + REVERB ─────────────────────────── const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 2000;
§ 03 — SpeechSynthesis + SpeechRecognition
Speech API
Two complementary APIs: SpeechSynthesis converts text to spoken audio (no server, no API key), and SpeechRecognition converts microphone input into text in real time.
Chrome/Edge only. Requires microphone permission.
// ── SPEECH SYNTHESIS (Text → Audio) ──────────── const utterance = new SpeechSynthesisUtterance('Hello!'); utterance.voice = speechSynthesis.getVoices()[0]; utterance.rate = 1; // 0.1 – 10 utterance.pitch = 1; // 0 – 2 utterance.volume = 1; // 0 – 1 utterance.lang = 'en-US'; // Events utterance.onstart = () => console.log('Speaking...'); utterance.onend = () => console.log('Done'); utterance.onboundary= e => console.log('Word:', e.charIndex); speechSynthesis.speak(utterance); speechSynthesis.pause(); speechSynthesis.resume(); speechSynthesis.cancel(); // ── SPEECH RECOGNITION (Audio → Text) ────────── const SR = window.SpeechRecognition || window.webkitSpeechRecognition; const recognition = new SR(); recognition.continuous = true; // keep listening recognition.interimResults= true; // show partial results recognition.lang = 'en-US'; recognition.onresult = e => { const transcript = [...e.results] .map(r => r[0].transcript).join(''); }; recognition.start(); recognition.stop();
§ 04 — navigator.clipboard
Clipboard API
Read from and write to the system clipboard asynchronously — copy code snippets, share URLs, paste rich text, even read images from the clipboard. Replaces the old document.execCommand.
navigator.clipboard.writeText('Hello!')
// ── WRITE (copy) ────────────────────────────── await navigator.clipboard.writeText('Hello!'); // Write rich HTML + plain text fallback const blob = new ClipboardItem({ 'text/html': new Blob(['<b>Bold</b>'], {type:'text/html'}), 'text/plain': new Blob(['Bold'], {type:'text/plain'}), }); await navigator.clipboard.write([blob]); // ── READ (paste) ─────────────────────────────── const text = await navigator.clipboard.readText(); const items = await navigator.clipboard.read(); for (const item of items) { for (const type of item.types) { const blob = await item.getType(type); if (type === 'image/png') { img.src = URL.createObjectURL(blob); } } } // ── COPY BUTTON PATTERN ──────────────────────── btn.addEventListener('click', async () => { await navigator.clipboard.writeText(code.textContent); btn.textContent = 'Copied!'; setTimeout(() => btn.textContent = 'Copy', 2000); });
§ 05 — IntersectionObserver API
Intersection Observer
Detect when elements enter or leave the viewport — without scroll event listeners. Use it for lazy-loading images, triggering animations on scroll, and implementing infinite scroll.
const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); io.unobserve(entry.target); // once } }); }, { threshold: 0.2, // 20% visible triggers rootMargin: '0px 0px -40px 0px' }); document.querySelectorAll('[data-reveal]') .forEach(el => io.observe(el)); // Lazy load images const imgIO = new IntersectionObserver(entries => { entries.forEach(({ isIntersecting, target }) => { if (isIntersecting) { target.src = target.dataset.src; // load now imgIO.unobserve(target); } }); }); document.querySelectorAll('img[data-src]') .forEach(img => imgIO.observe(img));
§ 06 — element.animate() / WAAPI
Web Animations API
The Web Animations API (WAAPI) brings CSS animation power to JavaScript — controllable, composable, and inspectable. Pause, reverse, and scrub animations programmatically.
const box = document.querySelector('.box'); // ── element.animate() ───────────────────────── const anim = box.animate( [ // keyframes { transform: 'translateX(0)', opacity: 1 }, { transform: 'translateX(200px)', opacity: 0.5, offset: 0.7 }, { transform: 'translateX(300px)', opacity: 1 }, ], { // options duration: 1200, easing: 'cubic-bezier(.34,1.56,.64,1)', iterations: 1, fill: 'forwards', delay: 200, } ); // ── CONTROL ─────────────────────────────────── anim.pause(); anim.play(); anim.reverse(); anim.currentTime = 500; // scrub to 500ms anim.playbackRate = 2; // double speed anim.cancel(); anim.onfinish = () => console.log('Done!'); // ── SEQUENCE with async/await ───────────────── async function sequence(el) { await el.animate([{transform:'scale(1)'},{transform:'scale(1.3)'}],{duration:300}).finished; await el.animate([{opacity:1},{opacity:0}],{duration:400}).finished; el.style.opacity = 1; }
§ 07 — type="color|date|range|datalist" + progress + meter + output
Native HTML5 Inputs
HTML5 added over a dozen new input types — no library needed for color pickers, date selectors, or range sliders. Plus the semantic <progress>, <meter>, and <output> elements.
§ 08 — Native <dialog> element
Native Dialog Element
The <dialog> element provides a native modal/non-modal with built-in focus trapping, Escape key handling, and a ::backdrop pseudo-element — no JavaScript modal library needed.
<dialog id="modal"> <h2>Confirm?</h2> <p>This action cannot be undone.</p> <form method="dialog"> <!-- auto-closes dialog --> <button value="cancel">Cancel</button> <button value="ok">OK</button> </form> </dialog> const dialog = document.getElementById('modal'); dialog.showModal(); // modal — blocks interaction dialog.show(); // modeless — non-blocking dialog.close(); // close programmatically dialog.close('saved'); // close with return value // Listen for close dialog.addEventListener('close', () => { console.log(dialog.returnValue); // "ok" or "cancel" }); /* CSS — style the backdrop */ dialog::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(6px); }
§ 09 — No-JavaScript accordion
Details & Summary
The <details> and <summary> elements create a disclosure widget — a native collapsible/expandable section with zero JavaScript. Style it with CSS for polished accordions.
What is the <details> element?
<summary> is the visible header; everything else is the collapsible content. No JavaScript, no ARIA attributes needed — the browser handles accessibility for you.Can I style it with CSS?
::marker pseudo-element controls the triangle, or you can hide it and use ::after for a custom icon. The details[open] selector lets you style the open state. Animate height with CSS Grid — grid-template-rows: 0fr → 1fr.Is it accessible?
group / button) built in. Keyboard navigable with Enter/Space. Screen readers announce the expanded/collapsed state automatically.The open attribute
open attribute in HTML (or details.open = true in JS) makes a section start expanded. You can also listen to the toggle event to react when the state changes.<!-- Zero JavaScript needed --> <details> <summary>Click me to expand</summary> <p>Hidden content revealed on click.</p> </details> <details open> <!-- open by default --> /* CSS — animated open */ details .body { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .3s ease; } details[open] .body { grid-template-rows: 1fr; /* smooth expand */ } /* Remove default marker triangle */ summary::-webkit-details-marker { display: none; } summary::marker { display: none; } summary::after { content: '▸'; transition: transform .25s; } details[open] summary::after { transform: rotate(90deg); } // JS — listen for toggle details.addEventListener('toggle', e => { console.log(details.open ? 'Opened' : 'Closed'); });
§ 10 — customElements.define()
Web Components
Define reusable HTML elements that work like native browser elements — complete with Shadow DOM encapsulation, lifecycle callbacks, and observed attributes. No framework required.
class ForestBadge extends HTMLElement { // Which attributes to observe static get observedAttributes() { return ['level', 'icon']; } connectedCallback() { // like componentDidMount this.attachShadow({ mode: 'open' }); this.render(); } attributeChangedCallback() { // re-render on change if (this.shadowRoot) this.render(); } render() { const level = this.getAttribute('level') || 'starter'; const icon = this.getAttribute('icon') || '🌱'; const label = this.textContent; this.shadowRoot.innerHTML = ` <style> :host { display: inline-flex; } .badge { padding: 6px 14px; border-radius: 20px; font-family: monospace; font-size: .8rem; } .starter { background: rgba(0,200,120,.15); color: #00c878; } .expert { background: rgba(232,184,75,.15); color: #e8b84b; } </style> <span class="badge ${level}">${icon} ${label}</span> `; } } // Register the element customElements.define('forest-badge', ForestBadge); // Use in HTML like a native element!
§ 11 — contenteditable + document.execCommand
ContentEditable Editor
The contenteditable attribute turns any element into a rich-text editor. Pair with document.execCommand() or the modern Selection API for a zero-dependency WYSIWYG.
§ 12 — Notification API + Push
Notifications API
Send native OS-level desktop notifications from the browser — the same ones your email client and apps use. Works even when the tab isn't focused (with Service Workers, even when closed).
// ── REQUEST PERMISSION ────────────────────────── const permission = await Notification.requestPermission(); // 'granted' | 'denied' | 'default' // ── SEND NOTIFICATION ─────────────────────────── if (Notification.permission === 'granted') { const n = new Notification('New Message', { body: 'Alice sent you a message', icon: '/icon-192.png', image: '/preview.jpg', badge: '/badge.png', tag: 'chat-123', // replaces previous with same tag requireInteraction: false, // auto-dismiss silent: false, data: { userId: 42 }, }); n.onclick = () => window.focus(); n.onclose = () => console.log('Dismissed'); n.close(); // dismiss programmatically } // ── PUSH via Service Worker (background) ──────── const reg = await navigator.serviceWorker.ready; reg.showNotification('Background Push', { body: 'Sent even when tab is closed', actions: [ { action: 'reply', title: 'Reply' }, { action: 'dismiss', title: 'Dismiss' }, ] });
§ 13 — Fullscreen API + Page Visibility API
Fullscreen & Page Visibility
The Fullscreen API lets any element take over the screen — great for media players, presentations, and games. Page Visibility detects when the user switches tabs or minimizes.
Switch to another tab and come back — the log updates!
// ── FULLSCREEN ───────────────────────────────── await element.requestFullscreen({ navigationUI: 'hide' }); await document.exitFullscreen(); const isFs = !!document.fullscreenElement; document.addEventListener('fullscreenchange', () => { if (document.fullscreenElement) console.log('Entered FS'); else console.log('Exited FS'); }); /* CSS fullscreen styling */ .player:fullscreen { background: black; } .player:fullscreen video { width: 100vw; } // ── PAGE VISIBILITY ──────────────────────────── document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { pauseVideo(); // user tabbed away saveProgress(); stopAnimationLoop(); } else { resumeVideo(); // user is back startAnimationLoop(); } }); // document.visibilityState: 'visible' | 'hidden' // document.hidden: true | false
§ 14 — ResizeObserver + MutationObserver
Resize & Mutation Observers
Observe element size changes without polling — react when a user resizes a panel, a font changes, or a sidebar collapses. MutationObserver watches any DOM change.
// ── RESIZE OBSERVER ──────────────────────────── const ro = new ResizeObserver(entries => { entries.forEach(entry => { const { width, height } = entry.contentRect; console.log(`Resized to ${width}×${height}px`); // Container query fallback entry.target.classList.toggle('compact', width < 400); }); }); ro.observe(document.querySelector('.sidebar')); ro.unobserve(el); ro.disconnect(); // ── MUTATION OBSERVER ────────────────────────── const mo = new MutationObserver(mutations => { mutations.forEach(m => { if (m.type === 'childList') { console.log('Added:', [...m.addedNodes]); console.log('Removed:',[...m.removedNodes]); } if (m.type === 'attributes') console.log(`Attr ${m.attributeName} changed`); if (m.type === 'characterData') console.log('Text changed'); }); }); mo.observe(document.body, { childList: true, // watch for added/removed nodes subtree: true, // watch all descendants attributes: true, // watch attribute changes characterData: true, // watch text node changes });
§ 15 — performance.now() + PerformanceObserver
Performance API
Sub-millisecond timing, memory stats, paint timing, and long-task detection — all built into the browser with no profiler needed. Essential for diagnosing slow interactions.
// ── HIGH-RESOLUTION TIMING ───────────────────── const t0 = performance.now(); // ms, 5μs precision heavyOperation(); const t1 = performance.now(); console.log(`Took ${(t1-t0).toFixed(3)}ms`); // ── MARKS + MEASURES ─────────────────────────── performance.mark('start'); doWork(); performance.mark('end'); performance.measure('total', 'start', 'end'); const [m] = performance.getEntriesByName('total'); console.log(m.duration); // ms // ── NAVIGATION TIMING ────────────────────────── const nav = performance.getEntriesByType('navigation')[0]; nav.domContentLoadedEventEnd - nav.startTime // DOMContentLoaded nav.loadEventEnd - nav.startTime // Page Load nav.responseEnd - nav.requestStart // TTFB // ── MEMORY (Chrome only) ─────────────────────── performance.memory.usedJSHeapSize // bytes used performance.memory.jsHeapSizeLimit // ── LONG TASKS ───────────────────────────────── new PerformanceObserver(list => { list.getEntries().forEach(entry => { console.warn(`Long task: ${entry.duration}ms`); }); }).observe({ entryTypes: ['longtask'] });
§ 16 — <picture> + srcset + sizes
Picture & Responsive Images
The <picture> element provides art direction and format negotiation — serve AVIF to browsers that support it, WebP as fallback, JPEG as last resort. srcset and sizes handle resolution switching.
Format fallback chain
Resolution switching
Native lazy loading
Off-main-thread decode
<!-- Format negotiation: AVIF → WebP → JPEG --> <picture> <source type="image/avif" srcset="hero.avif"> <source type="image/webp" srcset="hero.webp"> <img src="hero.jpg" alt="Hero" loading="eager" decoding="async"> </picture> <!-- Art direction: different crop at each breakpoint --> <picture> <source media="(min-width:1024px)" srcset="desktop.jpg"> <source media="(min-width:640px)" srcset="tablet.jpg"> <img src="mobile.jpg" alt="Team photo"> </picture> <!-- Resolution switching with srcset + sizes --> <img src="card-400.jpg" srcset="card-400.jpg 400w, card-800.jpg 800w, card-1600.jpg 1600w" sizes="(max-width:640px) 100vw, (max-width:1024px) 50vw, 400px" loading="lazy" decoding="async" alt="Responsive image">
§ 17 — getUserMedia / MediaDevices
WebRTC — Camera & Mic
Access the device camera and microphone directly with getUserMedia() — take photos, record video, scan QR codes, detect faces, or build video conferencing, all in pure HTML5.
// ── GET CAMERA STREAM ────────────────────────── const stream = await navigator.mediaDevices .getUserMedia({ video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user', // 'user' | 'environment' }, audio: false }); video.srcObject = stream; // ── LIST DEVICES ─────────────────────────────── const devices = await navigator.mediaDevices.enumerateDevices(); const cameras = devices.filter(d => d.kind === 'videoinput'); // ── TAKE SNAPSHOT ────────────────────────────── const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); const dataUrl = canvas.toDataURL('image/jpeg', 0.9); // ── STOP STREAM ──────────────────────────────── stream.getTracks().forEach(t => t.stop());
§ 18 — navigator.serviceWorker
Service Workers & PWA
Service Workers are a proxy that runs between your page and the network — enabling offline caching, background sync, and push notifications. They're the engine of Progressive Web Apps.
Registration
The browser downloads and registers sw.js in the background on first visit.
Installation
The SW installs and pre-caches your app shell — HTML, CSS, fonts, icons.
Activation
Old caches are cleaned up. The SW takes control of all pages in scope.
Fetch interception
Every network request passes through the SW — cache-first, network-first, stale-while-revalidate strategies.
// ── REGISTER (in main.js) ────────────────────── if ('serviceWorker' in navigator) { const reg = await navigator.serviceWorker .register('/sw.js', { scope: '/' }); reg.addEventListener('updatefound', () => { console.log('New SW version installing...'); }); } // ── sw.js (runs in separate thread) ─────────── const CACHE = 'v1-shell'; const ASSETS = ['/', '/app.css', '/app.js', '/fonts/...']; self.addEventListener('install', e => { e.waitUntil( caches.open(CACHE).then(c => c.addAll(ASSETS)) ); }); // Cache-first strategy (offline works!) self.addEventListener('fetch', e => { e.respondWith( caches.match(e.request).then(cached => cached || fetch(e.request) // fallback to network ) ); });
§ 19 — storage event + BroadcastChannel API
Cross-Tab Sync
Sync state across all open tabs without a server using the storage event (fires in other tabs when localStorage changes) or the newer BroadcastChannel API.
// ── METHOD 1: storage event ──────────────────── // Only fires in OTHER tabs, not the one that wrote window.addEventListener('storage', e => { console.log('Key changed:', e.key); console.log('Old value:', e.oldValue); console.log('New value:', e.newValue); console.log('In tab:', e.url); }); // Write from tab A — tab B gets the event localStorage.setItem('cart', 'updated'); // ── METHOD 2: BroadcastChannel API ──────────── const ch = new BroadcastChannel('app-channel'); // Send to ALL tabs (including this one!) ch.postMessage({ type: 'theme-change', dark: true }); // Receive ch.onmessage = e => { const { type, dark } = e.data; if (type === 'theme-change') document.body.classList.toggle('dark', dark); }; // Cleanup ch.close(); // ── METHOD 3: SharedWorker ───────────────────── // A single JS thread shared across ALL tabs const worker = new SharedWorker('shared.js'); worker.port.postMessage('ping'); worker.port.onmessage = e => console.log(e.data);
§ 20 — setProperty + CSS.registerProperty
CSS Properties via JS
JavaScript can read and write CSS custom properties at runtime — enabling live theming, user preferences, and dynamic design systems without touching element styles directly.
Live Theme Preview
All CSS variables are updated in real-time by writing to document.documentElement.style.setProperty(). No class toggling, no inline styles on individual elements.
const root = document.documentElement; // ── WRITE ────────────────────────────────────── root.style.setProperty('--primary', '#00c878'); root.style.setProperty('--radius', '12px'); root.style.setProperty('--font-size', '16px'); // ── READ ─────────────────────────────────────── getComputedStyle(root).getPropertyValue('--primary'); // ── REMOVE ───────────────────────────────────── root.style.removeProperty('--primary'); // ── THEME SYSTEM ────────────────────────────── function applyTheme(theme) { Object.entries(theme).forEach(([k, v]) => { root.style.setProperty(k, v); }); localStorage.setItem('theme', JSON.stringify(theme)); } // ── REGISTER typed property (Houdini) ───────── CSS.registerProperty({ name: '--hue', syntax: '<number>', initialValue: '160', inherits: false, }); // Now --hue can be transitioned / animated!