Each challenge says what you should see when it works. Try it with the starter code, then compare with the solution.
Challenge 1: toggle dark mode
Make the button toggle a dark class on the page body. The text should switch between “Dark mode” and “Light mode”.
Expected: each click switches the background colour and the button text.
<style>
body { font-family: system-ui, sans-serif; padding: 12px; }
body.dark { background: #1c1b1f; color: #eee; }
</style>
<button id="theme">Dark mode</button>
Show a possible solution
<style>
body { font-family: system-ui, sans-serif; padding: 12px; }
body.dark { background: #1c1b1f; color: #eee; }
</style>
<button id="theme" aria-pressed="false">Dark mode</button>
<script>
const button = document.querySelector("#theme");
button.addEventListener("click", () => {
const dark = document.body.classList.toggle("dark");
button.textContent = dark ? "Light mode" : "Dark mode";
button.setAttribute("aria-pressed", String(dark));
});
</script>
Challenge 2: character counter
Show “n / 40” under a textarea that updates as the user types. Turn the text red when fewer than 5 characters remain.
Expected: typing 36 characters shows 36 / 40 in red.
Show a possible solution
<textarea id="t" maxlength="40" aria-describedby="c"></textarea>
<p id="c">0 / 40</p>
<script>
const t = document.querySelector("#t");
const c = document.querySelector("#c");
t.addEventListener("input", () => {
c.textContent = `${t.value.length} / 40`;
c.style.color = 40 - t.value.length < 5 ? "crimson" : "";
});
</script>
Challenge 3: a live clock
Show the current time and update it every second. Stop the interval when the button is pressed.
Expected: the time ticks, then freezes after pressing the button.
Show a possible solution
<p id="clock"></p>
<button id="stop">Stop</button>
<script>
const clock = document.querySelector("#clock");
const tick = () => { clock.textContent = new Date().toLocaleTimeString(); };
tick();
const timer = setInterval(tick, 1000);
document.querySelector("#stop").addEventListener("click", () => clearInterval(timer));
</script>
Challenge 4: delete from a list with delegation
Given a list of items with a “x” button each, delete the item when its button is clicked, using one event listener.
Expected: clicking any “x” removes its list item. New items you add also work.
Show a possible solution
<ul id="list">
<li>Apples <button>x</button></li>
<li>Bread <button>x</button></li>
</ul>
<button id="add">Add item</button>
<script>
const list = document.querySelector("#list");
list.addEventListener("click", event => {
const button = event.target.closest("button");
if (button) button.closest("li").remove();
});
document.querySelector("#add").addEventListener("click", () => {
const li = document.createElement("li");
li.append("New item ", Object.assign(document.createElement("button"), { textContent: "x" }));
list.append(li);
});
</script>
Challenge 5: a keyboard shortcut
Pressing / should focus the search field (but only when the user is not already typing in a field). Pressing Escape in the field should clear it.
Expected: press / anywhere on the page, and the cursor jumps to the search box.
Show a possible solution
<input id="search" placeholder="Search (press / to focus)">
<script>
const search = document.querySelector("#search");
document.addEventListener("keydown", event => {
const typing = ["INPUT", "TEXTAREA", "SELECT"].includes(document.activeElement.tagName);
if (event.key === "/" && !typing) {
event.preventDefault();
search.focus();
}
});
search.addEventListener("keydown", event => {
if (event.key === "Escape") search.value = "";
});
</script>
Challenge 6: show and hide a password
Add a checkbox “Show password” that switches the input between type="password" and type="text".
Expected: checking the box reveals the characters, unchecking hides them again.
Show a possible solution
<label>Password <input id="pw" type="password" value="secret123"></label>
<label><input type="checkbox" id="show"> Show password</label>
<script>
document.querySelector("#show").addEventListener("change", event => {
document.querySelector("#pw").type = event.target.checked ? "text" : "password";
});
</script>
Challenge 7: filter a list as you type
Hide the list items that do not contain the text typed in a box, ignoring case.
Expected: typing “an” leaves only items containing “an” (Banana, Mango).
Show a possible solution
<input id="q" placeholder="Filter fruit">
<ul id="fruit"><li>Apple</li><li>Banana</li><li>Cherry</li><li>Mango</li></ul>
<script>
document.querySelector("#q").addEventListener("input", event => {
const term = event.target.value.toLowerCase();
document.querySelectorAll("#fruit li").forEach(li => {
li.hidden = !li.textContent.toLowerCase().includes(term);
});
});
</script>
Challenge 8: a form check
A form has an email field. On submit, prevent the page from reloading, check the email with a simple pattern, and show either an error message or “Thanks!” in a status paragraph.
Expected: maya@example.com shows “Thanks!”, while maya shows an error.
Show a possible solution
<form id="f" novalidate>
<label>Email <input id="email" name="email"></label>
<button>Join</button>
</form>
<p id="status" role="status"></p>
<script>
document.querySelector("#f").addEventListener("submit", event => {
event.preventDefault();
const value = document.querySelector("#email").value.trim();
const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
document.querySelector("#status").textContent = valid ? "Thanks!" : "Please enter a valid email address.";
});
</script>
Next, find the bugs in Debugging Challenges.