Interactive Static Websites

JavaScript
Without
a Server

Twenty fully-working JavaScript patterns for modern static websites — DOM manipulation, live filtering, fetch APIs, shopping carts, drag and drop, toast notifications, Canvas charts, hash routing, keyboard shortcuts, and live dashboards. No frameworks, no build tools, no backend.

DOM API Fetch localStorage Canvas ● No Framework

§ 01 — Document Object Model

DOM Manipulation

The DOM is your JavaScript interface to the HTML page. Master these patterns — they underpin everything else: finding elements, reading and writing content, toggling classes, and storing data on elements.

LIVE — DOM interaction lab
Target element — click buttons above to manipulate me
dom-essentials.js
// ── FINDING ELEMENTS ──────────────────────────
const el  = document.querySelector('.card');           // first match
const all = document.querySelectorAll('.card');        // NodeList
const byId= document.getElementById('header');        // fastest

// ── READING & WRITING CONTENT ─────────────────
el.textContent = 'Safe text';          // escaped, no XSS risk
el.innerHTML   = '<strong>HTML</strong>'; // renders HTML tags
const txt = el.textContent;             // read text

// ── CLASSES ───────────────────────────────────
el.classList.add('active');            // add
el.classList.remove('active');         // remove
el.classList.toggle('open');           // flip
el.classList.toggle('open', condition); // set/unset
el.classList.contains('active');       // → boolean

// ── DATA ATTRIBUTES ───────────────────────────
// <div data-user-id="42" data-role="admin">
el.dataset.userId = '42';             // set
const id = el.dataset.userId;          // read → "42"

// ── CREATING & INSERTING ──────────────────────
const card = document.createElement('article');
card.className = 'card';
card.innerHTML = `<h3>${title}</h3><p>${desc}</p>`;
container.appendChild(card);          // add at end
container.prepend(card);              // add at start
target.insertAdjacentHTML('beforeend', html); // flexible
// positions: beforebegin, afterbegin, beforeend, afterend

// ── REMOVING ─────────────────────────────────
el.remove();                           // remove from DOM
parent.removeChild(child);             // old way

// ── INLINE STYLES & CSS PROPERTIES ───────────
el.style.color     = 'red';
el.style.transform = 'scale(1.1)';
el.style.setProperty('--accent', '#gold'); // CSS variable!

§ 02 — Event-Driven Programming

Event Handling

JavaScript is event-driven — code runs in response to user actions. Master event delegation, custom events, and keyboard/pointer patterns. These are the building blocks of all interactive UI.

Event delegation
🎨 Design Project click me
⚡ Dev Project click me
📱 Mobile App click me
Click any card — one listener handles all
events.js
// ── DELEGATION — one listener for all ─────
document.getElementById('list')
  .addEventListener('click', e => {
    const card = e.target.closest('.card');
    if (!card) return;
    console.log('Card:', card.dataset.id);
  });

// ── LISTENER OPTIONS ──────────────────────
el.addEventListener('click', handler, {
  once:    true,    // auto-removes after 1 fire
  passive: true,    // can't preventDefault (perf)
  capture: false,   // bubble vs capture phase
});

// ── CUSTOM EVENTS ─────────────────────────
// Dispatch
el.dispatchEvent(new CustomEvent('cart:add', {
  detail:  { item, qty },
  bubbles: true,
}));

// Listen anywhere in the DOM
document.addEventListener('cart:add', e => {
  updateCartUI(e.detail.item);
});

// ── KEYBOARD ──────────────────────────────
document.addEventListener('keydown', e => {
  if (e.key === 'Escape')      closeModal();
  if (e.key === '/')           focusSearch();
  if (e.ctrlKey && e.key='k') openCommand();
});

§ 03 — Rendering UI from Data

Dynamic Rendering

Keep your data separate from your HTML. Store data as JavaScript arrays/objects, then render them into the DOM using map() and template literals. Re-render on data change — no framework needed.

LIVE — Add, edit, delete from a JS array
render-from-data.js
// ── RENDER PATTERN — data drives the UI ───
let tasks = [];   // source of truth

const TaskCard = (task) => `
  <div class="task-card ${task.done ? 'done' : ''}"
       data-id="${task.id}">
    <input type="checkbox" ${task.done ? 'checked' : ''}
           onchange="toggleTask(${task.id})">
    <span>${task.text}</span>
    <button onclick="deleteTask(${task.id})">✕</button>
  </div>
`;

function render() {
  document.getElementById('list').innerHTML =
    tasks.length
      ? tasks.map(TaskCard).join('')
      : '<p class="empty">No tasks yet</p>';
}

function addTask(text) {
  tasks.push({ id: Date.now(), text, done: false });
  saveTasks(); render();
}

function toggleTask(id) {
  tasks = tasks.map(t =>
    t.id === id ? { ...t, done: !t.done } : t
  );
  saveTasks(); render();
}

function saveTasks() {
  localStorage.setItem('tasks', JSON.stringify(tasks));
}

§ 04 — Live Filtering

Filter & Search

A real-time portfolio filter: category buttons hide/show cards instantly, a text search narrows further, and results count shows live feedback. All from a single JS data array — no server.

LIVE — Filter by category + text search
filter-search.js
const projects = [
  { id:1, title:'Design System',  cat:'design', tags:['css','figma']  },
  { id:2, title:'Portfolio Site', cat:'web',    tags:['html','js']   },
  // ...
];

let activeCategory = 'all';

function applyFilter() {
  const query = document.getElementById('search').value
    .toLowerCase().trim();

  const visible = projects.filter(p => {
    const matchCat  = activeCategory === 'all' || p.cat === activeCategory;
    const matchText = !query
      || p.title.toLowerCase().includes(query)
      || p.tags.some(t => t.includes(query));
    return matchCat && matchText;
  });

  document.querySelectorAll('.port-card').forEach(card => {
    const id = +card.dataset.id;
    card.classList.toggle('hidden', !visible.some(p => p.id === id));
  });

  document.getElementById('count').textContent =
    `${visible.length} of ${projects.length} results`;
}

§ 05 — Asynchronous Data

Fetch & Live APIs

Call any public REST API from JavaScript — show loading skeletons while waiting, handle errors gracefully, and cache results in localStorage to avoid redundant network requests.

LIVE — Fetching from JSONPlaceholder API
fetch-patterns.js
// ── COMPLETE FETCH PATTERN ────────────────
async function fetchData(url, options = {}) {
  const { cacheKey, cacheTTL = 300000 } = options; // 5min

  // Check localStorage cache first
  if (cacheKey) {
    const cached = JSON.parse(localStorage.getItem(cacheKey));
    if (cached && Date.now() - cached.ts < cacheTTL) {
      return cached.data;   // ← instant, no network
    }
  }

  showSkeleton();

  try {
    const res = await fetch(url);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    const data = await res.json();

    if (cacheKey) {
      localStorage.setItem(cacheKey, JSON.stringify({
        data, ts: Date.now()
      }));
    }

    hideSkeleton(); renderData(data);
    return data;

  } catch (err) {
    hideSkeleton();
    showError(`Could not load data: ${err.message}`);
    return null;
  }
}

// Usage
const posts = await fetchData(
  'https://jsonplaceholder.typicode.com/posts?_limit=5',
  { cacheKey: 'posts-cache', cacheTTL: 60000 }
);

§ 06 — Wizard Pattern

Multi-Step Form

Break long forms into steps. Validate each step before proceeding. Show a progress indicator. On the final step, show a summary before submission. Auto-saves progress to localStorage so users don't lose work.

LIVE — 3-step form wizard
1
Account
2
Profile
3
Review
0/200
Review your information before submitting. Click Back to make changes.

§ 07 — Animated Numbers & Time

Counters & Timers

Animated number counters make statistics feel alive. Countdown timers add urgency. A stopwatch is a classic JS interval pattern. All from requestAnimationFrame and setInterval.

LIVE — Animated counters
0
Developers
0
Projects Built
0
% Uptime
0
Countries
Countdown + Stopwatch
Countdown to New Year:
Stopwatch:
00:00.00

§ 09 — E-commerce Pattern

Shopping Cart

A complete shopping cart: add/remove items, update quantities, show a live total, persist cart in localStorage, and display a badge counter. This pattern works for any product catalog.

LIVE — Add items, cart persists on reload
Cart 0
Cart is empty

§ 10 — Drag API

Drag & Drop

The HTML Drag and Drop API lets users reorder lists and move items between columns. Combines dragstart, dragover, drop events with visual feedback classes.

Reorderable list
  • Design mockups
  • Write copy
  • Code components
  • Review & test
  • Deploy to production
Kanban columns
Todo
🎨 Design
📝 Copy
In Progress
💻 Code
Done
drag-drop.js
let dragEl = null;

// On every draggable item:
item.addEventListener('dragstart', e => {
  dragEl = item;
  e.dataTransfer.effectAllowed = 'move';
  setTimeout(() => item.classList.add('dragging'), 0);
});

item.addEventListener('dragend', () => {
  dragEl = null;
  item.classList.remove('dragging');
});

// On the list container:
list.addEventListener('dragover', e => {
  e.preventDefault();
  const after = getDragAfterElement(list, e.clientY);
  if (after == null) list.appendChild(dragEl);
  else list.insertBefore(dragEl, after);
});

function getDragAfterElement(container, y) {
  const els = [...container.querySelectorAll('.item:not(.dragging)')];
  return els.reduce((closest, el) => {
    const box    = el.getBoundingClientRect();
    const offset = y - box.top - box.height / 2;
    if (offset < 0 && offset > closest.offset)
      return { offset, element: el };
    return closest;
  }, { offset: -Infinity }).element;
}

§ 11 — Notification System

Toast Notifications

A production-quality toast notification system: queue management, four types (success/error/warning/info), auto-dismiss with animated progress bar, and manual close. Stacks cleanly in the corner.

LIVE — Click to fire notifications (look bottom-right)
toast-system.js
const icons = {success:'✓',error:'✗',warning:'⚠',info:'ℹ'};

function toast(type, title, msg, duration = 4000) {
  const el = document.createElement('div');
  el.className = `toast toast-${type}`;
  el.innerHTML = `
    <span class="toast-icon">${icons[type]}</span>
    <div class="toast-body">
      <div class="toast-title">${title}</div>
      <div class="toast-msg">${msg}</div>
    </div>
    <button class="toast-close" onclick="dismissToast(this)">✕</button>
    <div class="toast-progress" style="background:currentColor;width:100%"></div>
  `;

  document.getElementById('toast-container').appendChild(el);

  // Trigger animation
  requestAnimationFrame(() => el.classList.add('show'));

  // Shrink progress bar
  const bar = el.querySelector('.toast-progress');
  bar.style.transition = `width ${duration}ms linear`;
  requestAnimationFrame(() => bar.style.width = '0');

  // Auto dismiss
  setTimeout(() => dismissToast(el), duration);
}

function dismissToast(el) {
  const toast = el.closest ? el.closest('.toast') : el;
  toast.classList.remove('show');
  setTimeout(() => toast.remove(), 350);
}

§ 12 — Animated Typography

Text Effects

Typewriter, scramble, and split-text animations add personality without being distracting. All done with pure JavaScript intervals and timeouts — no animation libraries.

Typewriter effect
Scramble text — hover
Hover to scramble
JavaScript Magic
Interactive Web
text-effects.js
// ── TYPEWRITER ────────────────────────────
const phrases = ['Build static websites.','Ship without a server.','Use pure JavaScript.'];
let pIdx=0, cIdx=0, isDeleting=false, twTimer=null;

function typewriterTick() {
  const phrase = phrases[pIdx];
  const el     = document.getElementById('tw-text');
  el.textContent = phrase.slice(0, cIdx);

  if (!isDeleting && cIdx === phrase.length) {
    setTimeout(() => { isDeleting = true; }, 1600);
  } else if (isDeleting && cIdx === 0) {
    isDeleting = false;
    pIdx = (pIdx + 1) % phrases.length;
  }

  cIdx += isDeleting ? -1 : 1;
  twTimer = setTimeout(typewriterTick, isDeleting ? 50 : 90);
}

// ── SCRAMBLE TEXT ─────────────────────────
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01!';

function scramble(el) {
  const target = el.dataset.text;
  let iter = 0;
  const id = setInterval(() => {
    el.textContent = target.split('').map((ch, i) =>
      i < iter ? ch
               : chars[Math.floor(Math.random() * chars.length)]
    ).join('');
    if (iter >= target.length) clearInterval(id);
    iter += 0.35;
  }, 40);
}

§ 13 — Data Visualization

Canvas Charts

Build bar, donut, and line charts from scratch with the Canvas 2D API. No Chart.js needed for simple charts. Animating them in on load makes data feel dynamic and alive.

Bar Chart
Donut Chart
Line Chart
canvas-charts.js
// ── ANIMATED BAR CHART ────────────────────
function drawBarChart(canvas, data, colors) {
  const ctx   = canvas.getContext('2d');
  const { width: W, height: H } = canvas;
  const max   = Math.max(...data.map(d => d.value));
  const pad   = { top:20, bot:30, left:30, right:10 };
  const bw    = (W - pad.left - pad.right) / data.length - 8;
  let   prog  = 0;

  function draw(t) {
    ctx.clearRect(0, 0, W, H);
    data.forEach((d, i) => {
      const bh = ((H - pad.top - pad.bot) * d.value / max) * t;
      const x  = pad.left + i * (bw + 8);
      const y  = H - pad.bot - bh;
      ctx.fillStyle = colors[i % colors.length];
      ctx.roundRect(x, y, bw, bh, [4,4,0,0]);
      ctx.fill();
      ctx.fillStyle = '#9d9d9d';
      ctx.font = '10px Fira Code';
      ctx.fillText(d.label, x, H - 8);
    });
  }

  function animate(ts) {
    prog = Math.min(prog + 0.03, 1);
    draw(easeOut(prog));
    if (prog < 1) requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
}

§ 14 — Browser Platform Actions

Clipboard, Share & Print

Copy-to-clipboard buttons, the Web Share API (native share sheet on mobile), and print-optimized CSS — three patterns that make your content easier to distribute without any backend.

LIVE — Try each action
npm install @company/design-system
clipboard-share.js
// ── CLIPBOARD API ─────────────────────────
async function copyCode(btn, targetId) {
  const text = document.getElementById(targetId).textContent.trim();
  await navigator.clipboard.writeText(text);
  btn.textContent = 'Copied!';
  btn.classList.add('copied');
  setTimeout(() => {
    btn.textContent = 'Copy';
    btn.classList.remove('copied');
  }, 2000);
}

// ── WEB SHARE API (mobile native sheet) ───
async function shareNative() {
  if (!navigator.share) {
    return fallbackCopy(window.location.href);
  }
  await navigator.share({
    title: document.title,
    text:  'Check out this JavaScript guide!',
    url:   window.location.href,
  });
}

/* ── PRINT CSS ─────────────────────────────
   Put this in a <style media="print"> block: */
@media print {
  .no-print { display: none !important; } /* hide nav, btns */
  body       { background: white; color: black; }
  a::after   { content: ' (' attr(href) ')'; font-size:.8em; }
  h2, h3     { page-break-after: avoid; }
  .section   { page-break-inside: avoid; }
}

§ 15 — Infinite / Virtual Feed

Infinite Scroll

Load more content automatically when the user scrolls near the bottom — using IntersectionObserver on a sentinel element. Much better than a "Load More" button for content feeds.

LIVE — Scroll down to load more posts
Loading...
infinite-scroll.js
let page      = 1;
const PAGE_SIZE = 5;
let loading   = false;
let hasMore   = true;

const sentinel = document.getElementById('load-trigger');

const io = new IntersectionObserver(async ([entry]) => {
  if (!entry.isIntersecting || loading || !hasMore) return;
  loading = true;
  sentinel.textContent = 'Loading...';

  const data = await fetchPage(page++);

  if (data.length < PAGE_SIZE) {
    hasMore = false;
    sentinel.textContent = 'No more posts';
    io.unobserve(sentinel);
  } else {
    sentinel.textContent = '';
  }

  appendItems(data);
  loading = false;
}, { rootMargin: '200px' });  // load 200px before bottom

io.observe(sentinel);

// Skeleton loading during fetch
function showSkeletons(n = 3) {
  const html = Array.from({length:n}, () => `
    <div class="feed-item">
      <div class="skeleton" style="width:38px;height:38px;border-radius:50%"></div>
      <div style="flex:1">
        <div class="skeleton" style="height:12px;width:40%;margin-bottom:6px"></div>
        <div class="skeleton" style="height:10px;width:90%"></div>
      </div>
    </div>
  `).join('');
  document.getElementById('feed').insertAdjacentHTML('beforeend', html);
}

§ 16 — User Settings

Preferences System

A persistent user preferences system — font size, accent color, layout density, and theme. All stored in localStorage and applied immediately via CSS custom properties. No page reload needed.

LIVE — Settings persist across reloads
Font Size
Base text size for the interface
14px
Accent Color
Primary highlight color
Layout Density
Spacing between elements

Preview

This text responds to your preferences. Try changing the font size, accent color, or density above.

§ 17 — Single-Page App Routing

Hash Routing

Build a multi-view app without a server by using URL hash navigation. The browser fires a hashchange event when the URL hash changes — render different views in response. Shareable, bookmarkable URLs.

LIVE — Full SPA routing, no server needed
Home Projects About Contact

Welcome Home

This is the home view. Click the tabs above to navigate between views — no page reload, no server. The URL hash updates so it's shareable and works with the back button.

Projects

Here you'd render your portfolio items. Each route can load different data, render different components, and have its own URL: #/projects

About Me

Bio, skills, experience. This view is only rendered when navigated to — saving initial page load time. Params work too: #/projects/42

Contact

Contact form lives here. Navigate away and come back — the router re-renders it cleanly. Guards work too: redirect to login if not authenticated before showing this view.

hash-router.js
// ── HASH ROUTER ───────────────────────────
const routes = {
  '#/home':    () => renderHome(),
  '#/about':   () => renderAbout(),
  '#/projects':() => renderProjects(),
  '#/project': (id) => renderProject(id),
};

function handleRoute() {
  const hash  = window.location.hash || '#/home';
  const [path, ...params] = hash.split('/');
  const route = routes[path];

  if (route) { route(...params); }
  else        { render404(); }

  // Update nav active state
  document.querySelectorAll('.nav-link').forEach(a => {
    a.classList.toggle('active', a.href.endsWith(path));
  });
}

window.addEventListener('hashchange', handleRoute);
handleRoute(); // run on page load

// Navigate programmatically
function goTo(path, params = {}) {
  const query = new URLSearchParams(params).toString();
  window.location.hash = query ? `${path}?${query}` : path;
}

§ 18 — Global Shortcut System

Keyboard Shortcuts

Power users love keyboard shortcuts. A shortcut system registers global key combinations, displays a help overlay when pressing ?, and respects text inputs (doesn't fire while typing).

LIVE — Press ? to open the shortcut guide
Active shortcuts on this page:
? Open shortcut guide
Ctrl + K Open command palette
/ Focus search
Esc Close any open overlay
keyboard-shortcuts.js
// ── SHORTCUT REGISTRY ─────────────────────
const shortcuts = [
  { keys: ['?'],            action: openShortcutGuide,   label: 'Open shortcut guide' },
  { keys: ['ctrl+k'],       action: openCommandPalette,  label: 'Command palette'      },
  { keys: ['/'],            action: focusSearch,         label: 'Focus search'         },
  { keys: ['escape'],       action: closeAllOverlays,    label: 'Close overlay'        },
  { keys: ['ctrl+shift+d'], action: toggleDarkMode,     label: 'Toggle dark mode'     },
];

document.addEventListener('keydown', e => {
  // Don't fire while user is typing in a field
  const tag = document.activeElement.tagName;
  if (['INPUT','TEXTAREA','SELECT'].includes(tag)) return;

  const combo = [
    e.ctrlKey  && 'ctrl',
    e.shiftKey && 'shift',
    e.altKey   && 'alt',
    e.key.toLowerCase()
  ].filter(Boolean).join('+');

  const sc = shortcuts.find(s => s.keys.includes(combo));
  if (sc) { e.preventDefault(); sc.action(); }
});

§ 19 — Real-Time UI Updates

Live Dashboard

A dashboard that updates stats and charts every few seconds using setInterval — simulating a live data feed. The same pattern works with real WebSockets or polling APIs.

LIVE — Stats update every 3 seconds
Active Users
↑ 0%
Revenue Today
↑ 0%
Activity Feed
live-dashboard.js
// ── LIVE DASHBOARD PATTERN ────────────────
let dashData = { users: 1420, revenue: 8240 };

function updateDashboard() {
  // Simulate data change
  const userDelta  = Math.round((Math.random() - 0.3) * 50);
  const revDelta   = Math.round((Math.random() - 0.2) * 300);

  dashData.users   = Math.max(0, dashData.users + userDelta);
  dashData.revenue = Math.max(0, dashData.revenue + revDelta);

  // Update DOM with animated number change
  animateValue('d-users',   dashData.users,   'k');
  animateValue('d-revenue', dashData.revenue, '$');

  // Update trend indicator
  setTrend('d-users-ch', userDelta);
  setTrend('d-rev-ch',   revDelta);

  // Add activity item
  addActivity(randomEvent());
}

// Start live updates
updateDashboard();
setInterval(updateDashboard, 3000);

// Stop when tab is hidden (saves resources!)
document.addEventListener('visibilitychange', () => {
  if (document.hidden) clearInterval(dashTimer);
  else                 dashTimer = setInterval(updateDashboard, 3000);
});

§ 20 — Professional Code Patterns

Pro Patterns

The patterns that separate beginner code from production code: async/await error handling, the observer pattern for decoupled components, memoization for performance, and the state machine for complex UI logic.

pro-patterns.js — patterns that scale
// ── 1. EVENT BUS — decouple components ────
const EventBus = {
  _handlers: {},
  on(event, fn)    { (this._handlers[event] ??= []).push(fn); },
  off(event, fn)   { this._handlers[event] = (this._handlers[event]??[]).filter(h=>h!==fn); },
  emit(event, data){ (this._handlers[event]??[]).forEach(fn=>fn(data)); },
};

EventBus.on('cart:updated', data => updateCartBadge(data.count));
EventBus.emit('cart:updated', { count: 3 });

// ── 2. MEMOIZE — cache expensive functions ─
function memoize(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) cache.set(key, fn(...args));
    return cache.get(key);
  };
}
const expensiveCalc = memoize(rawCalcFn);

// ── 3. STATE MACHINE — predictable UI ─────
const formMachine = {
  state:    'idle',
  transitions: {
    idle:       ['submitting'],
    submitting: ['success', 'error'],
    error:      ['idle'],
    success:    [],
  },
  transition(to) {
    if (!this.transitions[this.state].includes(to))
      throw new Error(`Invalid: ${this.state}${to}`);
    this.state = to;
    renderForm(this.state);
  }
};

// ── 4. RETRY WITH BACKOFF ─────────────────
async function fetchWithRetry(url, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await (await fetch(url)).json();
    } catch {
      if (attempt === maxRetries - 1) throw;
      await new Promise(r => setTimeout(r, 2 ** attempt * 500));
    }
  }
}

// ── 5. VIRTUAL LIST — render huge datasets ─
// Only render what's visible in viewport
function VirtualList({ items, rowHeight, visibleCount }) {
  let scrollTop = 0;
  const startIdx = Math.floor(scrollTop / rowHeight);
  const visible  = items.slice(startIdx, startIdx + visibleCount);
  // ... render only `visible`, position with padding
}
✓ Event Bus decouples components — cart, nav badge, and analytics all listen independently
✓ Memoization eliminates redundant calculations on re-renders
✓ State machines prevent impossible UI states (loading AND showing error simultaneously)
✓ Retry with exponential backoff handles flaky network connections gracefully

Keyboard Shortcuts

Open this guide?
Close overlayEsc
Focus search/
Command paletteCtrl+K
Next sectionJ
Prev sectionK
Go to topG G
Toggle sidebarB