HTML5 Browser Platform

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.

Canvas & Audio Speech API Web Components WebRTC Camera ● All Demos Live

§ 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.

LIVE — Draw with your mouse or finger
canvas-api.js
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.

LIVE — Click keys to play (or press A-K on keyboard)
web-audio.js
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.

LIVE — Text-to-Speech
Ready
LIVE — Speech-to-Text

Chrome/Edge only. Requires microphone permission.

Transcript will appear here...
Not recording
speech-api.js
// ── 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.

LIVE — Copy, Paste, and Clipboard Read
Snippet to copy
navigator.clipboard.writeText('Hello!')
Paste target
Clipboard log will appear here...
clipboard-api.js
// ── 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.

LIVE — Scroll to reveal cards
🌱Card 1 — I fade in when I enter the viewport
🌿Card 2 — Scroll down to reveal me
🌲Card 3 — Each card gets its own observer
🍀Card 4 — Lazy-load images this way too
🎋Card 5 — Zero scroll event listeners!
🪴Card 6 — GPU-composited, butter-smooth
intersection-observer.js
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.

LIVE — Programmatic Animations
web-animations-api.js
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.

LIVE — All Specialty Inputs
#00c878
No date selected
42
Start typing...
68%
55/100
100

§ 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.

LIVE — Open the native dialog

Native <dialog>

This uses the browser's built-in dialog element — focus is trapped inside, pressing Escape closes it, and the ::backdrop is pure CSS.

dialog-element.html
<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.

LIVE — Pure HTML accordion (no JS!)
What is the <details> element?
A native HTML disclosure widget. The <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?
Completely! The ::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?
Yes — it has the correct ARIA roles (group / button) built in. Keyboard navigable with Enter/Space. Screen readers announce the expanded/collapsed state automatically.
The open attribute
Adding the 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.
details-summary.html
<!-- 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.

LIVE — Custom Elements in use
Starter Growing Expert
Add a custom badge:
web-components.js
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.

LIVE — Rich Text Editor
Try formatting this text — bold, italic, colors, lists. Everything saves in the DOM!
0 words · 0 characters

§ 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).

LIVE — Request Permission & Send Notifications
notifications-api.js
// ── 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.

LIVE — Fullscreen
🌲
Click to go Fullscreen
Press Escape to exit
LIVE — Page Visibility

Switch to another tab and come back — the log updates!

Current: visible
fullscreen-visibility.js
// ── 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.

LIVE — Drag the box corner to resize
← Drag right edge to resize →
Width: — · Height: —
LIVE — Mutation Observer
Edit this text — mutations logged below
Mutations will log here...
observers.js
// ── 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.

LIVE — Performance Metrics
performance-api.js
// ── 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.

VISUAL — Image Loading Strategies
🖼
<picture> + <source>
Format fallback chain
📱
srcset + sizes
Resolution switching
loading="lazy"
Native lazy loading
🎨
decoding="async"
Off-main-thread decode
responsive-images.html
<!-- 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.

LIVE — Camera Access
📷
Camera will appear here after permission
Snapshot
webrtc-camera.js
// ── 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.

LIVE — Service Worker Status
1

Registration

The browser downloads and registers sw.js in the background on first visit.

2

Installation

The SW installs and pre-caches your app shell — HTML, CSS, fonts, icons.

3

Activation

Old caches are cleaned up. The SW takes control of all pages in scope.

4

Fetch interception

Every network request passes through the SW — cache-first, network-first, stale-while-revalidate strategies.

Checking Service Worker support...
sw.js + main.js
// ── 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.

LIVE — Open this page in a second tab and watch them sync
cross-tab-sync.js
// ── 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 — Live Theme Editor

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.

css-custom-props-js.js
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!