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.
§ 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.
// ── 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.
// ── 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.
// ── 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.
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.
// ── 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.
§ 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.
§ 08 — Image Gallery
Gallery & Lightbox
A responsive gallery with a keyboard-navigable lightbox. Click any image to open it full-size, use arrow keys or buttons to navigate, and press Escape to close. Touch-swipe supported.
const images = [ { src:'/img/1.jpg', caption:'Mountain Sunrise' }, { src:'/img/2.jpg', caption:'City at Night' }, // ... ]; let current = 0; function openLightbox(index) { current = index; const lb = document.getElementById('lightbox'); lb.classList.add('open'); document.body.style.overflow = 'hidden'; showImage(current); // Touch swipe support let startX = 0; lb.addEventListener('touchstart', e => { startX = e.touches[0].clientX; }, { passive:true }); lb.addEventListener('touchend', e => { const dx = e.changedTouches[0].clientX - startX; if (Math.abs(dx) > 50) navigate(dx > 0 ? -1 : 1); }, { passive:true }); } function navigate(dir) { current = (current + dir + images.length) % images.length; showImage(current); } document.addEventListener('keydown', e => { if (!document.querySelector('.lightbox.open')) return; if (e.key === 'ArrowRight') navigate(1); if (e.key === 'ArrowLeft') navigate(-1); if (e.key === 'Escape') closeLightbox(); });
§ 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.
§ 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.
- ⠿Design mockups
- ⠿Write copy
- ⠿Code components
- ⠿Review & test
- ⠿Deploy to production
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.
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 ──────────────────────────── 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.
// ── 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.
// ── 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.
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.
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.
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 ─────────────────────────── 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).
// ── 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 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.
// ── 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 }
✓ 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