Beginner Exercises
These exercises focus on the fundamentals: DOM manipulation, events, and basic JavaScript logic. Each one can be built in a single HTML file.
Exercise 1: Temperature Converter
Build a converter that transforms between Celsius and Fahrenheit in real time as the user types.
What you learn: DOM querying, input events, real-time calculation, two-way binding logic.
// HTML needed:
// <input id="celsius" type="number" placeholder="Celsius">
// <input id="fahrenheit" type="number" placeholder="Fahrenheit">
const celsiusInput = document.getElementById("celsius");
const fahrenheitInput = document.getElementById("fahrenheit");
celsiusInput.addEventListener("input", () => {
const c = parseFloat(celsiusInput.value);
fahrenheitInput.value = isNaN(c) ? "" : ((c * 9) / 5 + 32).toFixed(2);
});
fahrenheitInput.addEventListener("input", () => {
const f = parseFloat(fahrenheitInput.value);
celsiusInput.value = isNaN(f) ? "" : (((f - 32) * 5) / 9).toFixed(2);
});
Exercise 2: Simple Counter
A counter with increment, decrement, and reset buttons. The display turns red when the count is negative.
What you learn: State management with a variable, conditional DOM updates, button event handling.
// HTML needed:
// <p id="count">0</p>
// <button id="inc">+</button>
// <button id="dec">-</button>
// <button id="reset">Reset</button>
let count = 0;
const display = document.getElementById("count");
function updateDisplay() {
display.textContent = count;
display.style.color = count < 0 ? "red" : count > 0 ? "green" : "inherit";
}
document.getElementById("inc").addEventListener("click", () => { count++; updateDisplay(); });
document.getElementById("dec").addEventListener("click", () => { count--; updateDisplay(); });
document.getElementById("reset").addEventListener("click", () => { count = 0; updateDisplay(); });
Exercise 3: Word Counter
Display live word count, character count, and estimated reading time as the user types in a textarea.
What you learn: String methods, regex, computed properties from user input.
// HTML needed:
// <textarea id="editor" rows="8"></textarea>
// <div id="stats"></div>
const editor = document.getElementById("editor");
const stats = document.getElementById("stats");
editor.addEventListener("input", () => {
const text = editor.value;
const chars = text.length;
const words = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
const mins = Math.ceil(words / 200); // avg reading speed
stats.textContent = `Words: ${words} | Characters: ${chars} | ~${mins} min read`;
});
Exercise 4: Color Picker Preview
Show a live preview box that changes background color to match a hex color input field.
What you learn: Input type="color", change events, inline style manipulation.
// HTML needed:
// <input type="color" id="colorPicker" value="#3b82f6">
// <input type="text" id="hexInput" value="#3b82f6">
// <div id="preview" style="width:100px;height:100px;"></div>
const picker = document.getElementById("colorPicker");
const hexInput = document.getElementById("hexInput");
const preview = document.getElementById("preview");
function applyColor(color) {
preview.style.backgroundColor = color;
picker.value = color;
hexInput.value = color;
}
picker.addEventListener("input", e => applyColor(e.target.value));
hexInput.addEventListener("change", e => {
if (/^#[0-9a-f]{6}$/i.test(e.target.value)) applyColor(e.target.value);
});
applyColor("#3b82f6"); // initial
Intermediate Exercises
These exercises introduce more complex DOM patterns, data persistence, and API integration.
Exercise 5: Todo List (No Frameworks)
A complete todo list with add, complete, delete, and filter β stored in an array and re-rendered on state change.
What you learn: State-driven rendering, array methods, event delegation, data-attributes.
// State
let todos = [];
let filter = "all"; // "all" | "active" | "completed"
// DOM refs
const input = document.getElementById("todo-input");
const list = document.getElementById("todo-list");
const filters = document.getElementById("filters");
// Add todo
document.getElementById("add-btn").addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
todos.push({ id: Date.now(), text, done: false });
input.value = "";
render();
});
// Delegated events on list
list.addEventListener("click", e => {
const id = Number(e.target.closest("[data-id]")?.dataset.id);
if (!id) return;
if (e.target.matches(".toggle")) todos = todos.map(t => t.id === id ? {...t, done: !t.done} : t);
if (e.target.matches(".delete")) todos = todos.filter(t => t.id !== id);
render();
});
// Filter buttons
filters.addEventListener("click", e => {
if (e.target.matches("button")) { filter = e.target.dataset.filter; render(); }
});
function render() {
const visible = todos.filter(t =>
filter === "all" ? true : filter === "active" ? !t.done : t.done
);
list.innerHTML = visible.map(t => `
<li data-id="${t.id}">
<button class="toggle">${t.done ? "β
" : "β¬"}</button>
<span style="text-decoration:${t.done ? "line-through" : "none"}">${t.text}</span>
<button class="delete">ποΈ</button>
</li>
`).join("");
}
render();
Exercise 6: Simple Calculator
A four-operation calculator with a display, decimal support, and keyboard input handling.
What you learn: State machine thinking, expression evaluation, keyboard events.
let expression = "";
const display = document.getElementById("calc-display");
function updateDisplay(val) {
display.textContent = val || "0";
}
function handleInput(value) {
if (value === "=") {
try {
// Replace Γ and Γ· if used
const result = Function('"use strict"; return (' + expression + ')')();
expression = String(result);
updateDisplay(expression);
} catch {
updateDisplay("Error");
expression = "";
}
} else if (value === "C") {
expression = "";
updateDisplay("0");
} else if (value === "β«") {
expression = expression.slice(0, -1);
updateDisplay(expression || "0");
} else {
expression += value;
updateDisplay(expression);
}
}
// Button clicks
document.getElementById("calc-buttons").addEventListener("click", e => {
if (e.target.matches("button")) handleInput(e.target.dataset.value);
});
// Keyboard support
document.addEventListener("keydown", e => {
const allowed = "0123456789+-*/.=";
if (allowed.includes(e.key)) handleInput(e.key);
if (e.key === "Enter") handleInput("=");
if (e.key === "Backspace") handleInput("β«");
if (e.key === "Escape") handleInput("C");
});
Exercise 7: Fetch and Display API Data
Fetch a list of users from a public API and render them as cards with loading and error states.
What you learn: Fetch API, async/await, loading states, error handling, template rendering.
const container = document.getElementById("user-list");
async function loadUsers() {
container.innerHTML = '<p class="loading">Loadingβ¦</p>';
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
if (!res.ok) throw new Error(`Status ${res.status}`);
const users = await res.json();
container.innerHTML = users.map(user => `
<div class="user-card">
<h3>${user.name}</h3>
<p>π§ ${user.email}</p>
<p>π’ ${user.company.name}</p>
<p>π ${user.website}</p>
</div>
`).join("");
} catch (err) {
container.innerHTML = `<p class="error">Failed to load: ${err.message}</p>`;
}
}
loadUsers();
Exercise 8: Form Validator
Validate a registration form (name, email, password) with real-time feedback and submission prevention.
What you learn: Form events, regex validation, CSS class toggling, preventing default submission.
const rules = {
name: { test: v => v.trim().length >= 2, msg: "Name must be at least 2 characters" },
email: { test: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), msg: "Enter a valid email" },
password: { test: v => v.length >= 8 && /\d/.test(v), msg: "Min 8 chars, at least one number" },
};
function validateField(fieldName, value) {
const rule = rules[fieldName];
const isValid = rule.test(value);
const field = document.getElementById(fieldName);
const error = document.getElementById(`${fieldName}-error`);
field.classList.toggle("invalid", !isValid);
field.classList.toggle("valid", isValid);
error.textContent = isValid ? "" : rule.msg;
return isValid;
}
// Real-time validation
Object.keys(rules).forEach(name => {
document.getElementById(name).addEventListener("input", e => {
validateField(name, e.target.value);
});
});
// On submit
document.getElementById("signup-form").addEventListener("submit", e => {
e.preventDefault();
const allValid = Object.keys(rules).every(name => {
return validateField(name, document.getElementById(name).value);
});
if (allValid) alert("Form submitted successfully!");
});
Advanced Exercises
These exercises demonstrate patterns used in production JavaScript: debounce, pub/sub, localStorage persistence, and intersection observer for infinite scroll.
Exercise 9: Debounce Function Implementation
Implement debounce(fn, delay) from scratch and use it to optimize a search input that queries an API.
What you learn: Closures, higher-order functions, timers, performance optimization.
function debounce(fn, delay) {
let timerId;
return function (...args) {
clearTimeout(timerId);
timerId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Usage: search input that waits 400ms after user stops typing
async function searchUsers(query) {
if (!query) return;
const res = await fetch(`https://jsonplaceholder.typicode.com/users?q=${query}`);
const users = await res.json();
console.log(`Found ${users.length} users for "${query}"`);
}
const debouncedSearch = debounce(searchUsers, 400);
document.getElementById("search").addEventListener("input", e => {
debouncedSearch(e.target.value);
// Without debounce: fires on EVERY keystroke
// With debounce: fires only 400ms after last keystroke
});
Exercise 10: Simple Pub/Sub Event System
Build a publish/subscribe (event bus) pattern that decouples components so they can communicate without direct references.
What you learn: Design patterns, closures, Map data structure, event-driven architecture.
function createEventBus() {
const subscribers = new Map();
return {
subscribe(event, handler) {
if (!subscribers.has(event)) subscribers.set(event, new Set());
subscribers.get(event).add(handler);
// Return unsubscribe function
return () => subscribers.get(event).delete(handler);
},
publish(event, data) {
if (!subscribers.has(event)) return;
subscribers.get(event).forEach(handler => handler(data));
},
clear(event) {
subscribers.delete(event);
}
};
}
// Usage
const bus = createEventBus();
// Component A subscribes
const unsub = bus.subscribe("user:login", data => {
console.log("Header updated for:", data.name);
});
// Component B subscribes
bus.subscribe("user:login", data => {
console.log("Analytics: login event for", data.email);
});
// Somewhere in your auth code
bus.publish("user:login", { name: "Alice", email: "alice@example.com" });
// Header updated for: Alice
// Analytics: login event for alice@example.com
unsub(); // Component A unsubscribes
bus.publish("user:login", { name: "Bob", email: "bob@example.com" });
// Analytics: login event for bob@example.com (only Component B fires)
Header updated for: Alice
Analytics: login event for alice@example.com
Analytics: login event for bob@example.com
Exercise 11: localStorage Note-Taking App
A persistent note app that saves notes to localStorage β surviving page refreshes. Supports create, edit, and delete.
What you learn: localStorage CRUD, JSON serialization, state persistence, unique ID generation.
const STORAGE_KEY = "ylearner-notes";
// Load from localStorage or default to []
let notes = JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
function save() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
}
function addNote(text) {
notes.push({ id: Date.now(), text, createdAt: new Date().toLocaleString() });
save();
render();
}
function deleteNote(id) {
notes = notes.filter(n => n.id !== id);
save();
render();
}
function updateNote(id, text) {
notes = notes.map(n => n.id === id ? { ...n, text } : n);
save();
}
function render() {
const container = document.getElementById("notes-container");
container.innerHTML = notes.length === 0
? '<p class="empty">No notes yet. Add one above.</p>'
: notes.map(n => `
<div class="note" data-id="${n.id}">
<textarea class="note-text">${n.text}</textarea>
<div class="note-footer">
<small>${n.createdAt}</small>
<button class="delete-note">Delete</button>
</div>
</div>
`).join("");
}
// Add note
document.getElementById("add-note-btn").addEventListener("click", () => {
const textarea = document.getElementById("new-note");
const text = textarea.value.trim();
if (text) { addNote(text); textarea.value = ""; }
});
// Delete / edit via delegation
document.getElementById("notes-container").addEventListener("click", e => {
if (e.target.matches(".delete-note")) {
const id = Number(e.target.closest(".note").dataset.id);
deleteNote(id);
}
});
document.getElementById("notes-container").addEventListener("input", e => {
if (e.target.matches(".note-text")) {
const id = Number(e.target.closest(".note").dataset.id);
updateNote(id, e.target.value);
}
});
render(); // Initial render from stored data
Exercise 12: Infinite Scroll Simulation
Simulate infinite scroll by loading more items as the user approaches the bottom of the page, using the Intersection Observer API.
What you learn: IntersectionObserver API, async data loading patterns, DOM appending vs re-rendering, UX loading feedback.
let page = 1;
let loading = false;
const ITEMS_PER_PAGE = 10;
const feed = document.getElementById("feed");
const sentinel = document.getElementById("sentinel"); // empty div at bottom
// Simulated async data source
async function fetchItems(pageNum) {
await new Promise(r => setTimeout(r, 600)); // simulate network delay
return Array.from({ length: ITEMS_PER_PAGE }, (_, i) => ({
id: (pageNum - 1) * ITEMS_PER_PAGE + i + 1,
title: `Item #${(pageNum - 1) * ITEMS_PER_PAGE + i + 1}`,
body: `Content loaded on page ${pageNum}.`
}));
}
async function loadMore() {
if (loading) return;
loading = true;
sentinel.textContent = "Loadingβ¦";
const items = await fetchItems(page++);
items.forEach(item => {
const card = document.createElement("div");
card.className = "feed-card";
card.innerHTML = `<h3>${item.title}</h3><p>${item.body}</p>`;
feed.insertBefore(card, sentinel);
});
sentinel.textContent = "";
loading = false;
}
// Watch the sentinel element
const observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting) loadMore();
}, { rootMargin: "200px" }); // start loading 200px before reaching bottom
observer.observe(sentinel);
loadMore(); // initial load
scroll event for infinite scroll β it fires hundreds of times per second. IntersectionObserver is built for this use case: it runs off the main thread and calls your callback only when the element crosses the viewport boundary.
Exercise Overview Table
| Exercise | Level | Key Concepts | Time Estimate |
|---|---|---|---|
| Temperature Converter | Beginner | Input events, math | 15 min |
| Counter | Beginner | State, DOM updates | 10 min |
| Word Counter | Beginner | Regex, string methods | 15 min |
| Color Picker | Beginner | Input types, styles | 20 min |
| Todo List | Intermediate | State rendering, delegation | 45 min |
| Calculator | Intermediate | State machine, keyboard | 60 min |
| Fetch Users | Intermediate | Fetch, async/await, errors | 30 min |
| Form Validator | Intermediate | Regex, feedback UX | 40 min |
| Debounce | Advanced | Closures, HOF, timers | 25 min |
| Pub/Sub | Advanced | Design patterns, Map | 35 min |
| Notes App | Advanced | localStorage, CRUD | 60 min |
| Infinite Scroll | Advanced | IntersectionObserver | 45 min |
Learning Path Recommendation
Complete exercises in order if you're new to JavaScript. If you're preparing for interviews specifically, prioritize:
- Debounce (closures + higher-order functions β extremely common interview question)
- Pub/Sub (design patterns + Map)
- Todo List (state-driven rendering)
- Form Validator (regex + real-world UX)
Extension Challenges
Once you've finished each exercise, enhance it:
- Add a throttle function alongside debounce (Exercise 9)
- Add "once" and "off" methods to the pub/sub bus (Exercise 10)
- Add search/filter to the notes app (Exercise 11)
- Add a "no more items" state to infinite scroll (Exercise 12)
FAQ
Do I need a framework to do these exercises?
No β all exercises use vanilla JavaScript. Learning the fundamentals without a framework makes you a much stronger developer when you later pick up React, Vue, or Svelte.
How do I test my exercises?
Open the HTML file in a browser and use the DevTools console. For automated testing, you can wrap pure functions in Jest or Vitest test suites β great practice for real-world development.
Can I use these exercises in my portfolio?
Absolutely. Polish one or two of the intermediate or advanced exercises, host them on GitHub Pages, and link them on your resume. Live demos are far more compelling than code alone.
What's the difference between debounce and the pub/sub pattern?
Debounce is a performance utility β it controls when a function fires. Pub/sub is an architectural pattern β it decouples components so they communicate without knowing about each other.
What should I build after these exercises?
Move on to full projects: a weather app, quiz app, or expense tracker. These combine multiple patterns and are more representative of real-world JavaScript work.
Summary
- Beginner exercises: input events, DOM updates, string manipulation, basic state
- Intermediate: state-driven rendering, fetch, event delegation, regex validation
- Advanced: closures (debounce), design patterns (pub/sub), localStorage persistence, IntersectionObserver
- Every exercise is self-contained and runnable in a browser with no build tools
- Implementing debounce from memory is a real interview question at many companies
- The pub/sub pattern demonstrates knowledge of design patterns beyond basic CRUD
- Building a localStorage app shows understanding of persistence without a backend
- IntersectionObserver knowledge signals awareness of performance best practices
- State-driven rendering (update state β re-render) is the mental model behind every major JS framework