Where It Gets Interactive

The DOM & Events

You know HTML, CSS and JavaScript separately. Now connect them: the DOM is the bridge that lets your JavaScript read and rewrite the live page, and events let it respond to every click, key and tap.

15 modules ~65 min read Animated examples JavaScript required first
Module 1

What the DOM Is

When the browser loads your HTML, it doesn't just draw it — it builds a live model of the page in memory called the DOM (Document Object Model). JavaScript talks to this model, and the screen updates instantly.

The DOM is a tree. At the top is document. Under it, <html>, then <head> and <body>, and every tag you wrote becomes a node hanging off its parent. Changing a node changes what the visitor sees.

Animated example

HTML becomes a tree of nodes

document
<html>
<head>
<body>
<h1>
<p>
<button>
  1. At the root sits document — your entry point to the whole page from JavaScript.
  2. The <html> element is its only child.
  3. It branches into <head> and <body>.
  4. Inside the body, each tag — <h1>, <p> — is a node.
  5. A <button> node too. JavaScript can grab any of these and change it.

The DOM is not your HTML fileYour .html file is the starting blueprint. The DOM is the living version in memory. When JavaScript edits the DOM, the file on disk never changes — only what's on screen does.

Module 2

Selecting Elements

Before you can change something, you have to grab it. That means selecting a node from the DOM and storing it in a variable.

The one method to remember is document.querySelector(). You pass it a CSS selector — the exact same syntax you learned in the CSS course — and it returns the first matching element:

script.js
const title = document.querySelector("#title");   // by id
const btn   = document.querySelector(".btn");     // by class
const all   = document.querySelectorAll("li");   // ALL matches

NodeLists vs Arrays

When you use querySelectorAll, it returns a NodeList, not a regular JavaScript Array. While you can use .forEach() on a NodeList, it lacks useful array methods like .map(), .filter(), or .reduce(). You can easily convert it to a real array:

arrays.js
const nodes = document.querySelectorAll("li");
// Convert using Array.from() or the spread operator [...]
const nodeArray = [...nodes]; 
nodeArray.filter(el => el.textContent.includes("Done"));
Animated example

querySelector reaches into the tree

document.querySelector( ? )
<body>
<h1 id="title">
<p>
<button class="btn">
  1. querySelector("#title") finds the element whose id is title and returns it.
  2. querySelector(".btn") finds the first element with class btn. Same CSS selectors you already know.

Store selections in variablesSelecting the DOM has a small cost, so grab an element once, save it in a const, and reuse that variable — don't call querySelector over and over for the same element.

Module 3

Traversing the DOM

Sometimes you don't know the exact class or ID of an element, but you know where it is relative to another element. The DOM allows you to "walk" the tree — moving up, down, and sideways from any node.

  • element.parentElement — Move up one level (e.g., from <li> to <ul>).
  • element.children — Get a list of all elements directly inside this one.
  • element.nextElementSibling — Move to the next sibling element at the same level.
  • element.closest("selector") — Move up the tree until it finds the nearest ancestor matching the selector (very useful for events).
script.js
const item = document.querySelector(".active");
const list = item.parentElement;         // The <ul> holding this item
const next = item.nextElementSibling;    // The next <li>
const card = item.closest(".card");      // The closest parent with class .card

Why traverse?It's crucial when building dynamic interfaces where multiple identical elements exist (like a list of products). If a user clicks the "Delete" button inside product #42, you can use button.closest(".product-card") to find and delete exactly that card without needing to give every card a unique ID.

Module 4

Changing Content & Styles

Once you hold an element, you can rewrite its text, swap its HTML, and restyle it — all live, with no page reload.

  • element.textContent = "..." — change the text inside.
  • element.innerHTML = "..." — replace with new HTML.
  • element.style.color = "red" — set a CSS property directly.
  • element.classList.add("active") — add/remove/toggle a CSS class (the professional way).

Attributes & Custom Data

You can also read or change any HTML attribute (like src, href, disabled), and store custom data directly in the HTML using data-* attributes.

attributes.js
img.setAttribute("src", "new-image.jpg");  // Change an attribute
const link = btn.getAttribute("href");       // Read an attribute

// Given HTML: <button data-id="42" data-user-role="admin">
const id = btn.dataset.id;                 // "42"
const role = btn.dataset.userRole;         // "admin" (camelCase)
Animated example

Rewriting an element on the fly

const box = querySelector(".box")
Original text
  1. Grab the element and store it as box.
  2. box.textContent = "Updated!" instantly changes the words on screen.
  3. box.style.background = "#b534ff" restyles it directly.
  4. box.classList.add("active") applies a whole CSS class at once — the cleanest approach.
Module 5

Events & Listeners

An event is something that happens: a click, a keypress, a mouse move, a form submit. You tell the browser "when this event happens on this element, run this function." That function is the event handler.

script.js
const btn = document.querySelector(".btn");

btn.addEventListener("click", () => {
  alert("You clicked me!");
});

Read it plainly: "on the button, add an event listener for click; when it fires, run this arrow function." The listener waits patiently forever, reacting every single time.

Animated example

A click fires a handler

// handler runs:
"You clicked me!"
"You clicked me!" // again…
  1. The listener is attached and waiting. Nothing happens yet.
  2. The user moves to the button and clicks.
  3. The click event fires and the handler function runs.
  4. Click again and it runs again — a listener reacts every time, for as long as the page is open.

Common eventsclick, input (typing in a field), submit (a form), keydown (a key), mouseenter/mouseleave (hover). Same pattern for all: element.addEventListener("event", handler).

Module 6

Forms & The Event Object

When an event fires, the browser automatically passes an Event Object (usually named e or event) into your handler. This object contains all the details about what just happened: which key was pressed, where the mouse was, or what element triggered the event.

Stopping Default Behavior

The most common use of the event object is handling forms. By default, submitting a form refreshes the whole page. You almost never want this in a modern web app. We use e.preventDefault() to stop the browser's default action so our JavaScript can handle the data instead.

form.js
const form = document.querySelector("form");
const input = document.querySelector("#username");

form.addEventListener("submit", (e) => {
  e.preventDefault();                 // Stop page reload!
  
  const userName = input.value;       // Read what they typed
  console.log("Welcome, " + userName);
  input.value = "";                     // Clear the input field
});

Reading Input ValuesNotice that to read what a user typed in a text input (or text area), you don't use textContent. You use element.value. For checkboxes, you check element.checked (which returns true or false).

Module 7

Putting It Together: a Live Counter

Everything you've learned in three courses meets here. HTML for structure, CSS for looks, JavaScript + the DOM + an event to make it do something. This is a real, working app.

counter.js
let count = 0;
const display = document.querySelector("#count");
const btn = document.querySelector("#plus");

btn.addEventListener("click", () => {
  count++;                       // change the data
  display.textContent = count;  // update the DOM
});
Animated example

Data changes, the DOM follows

0
count = 0
  1. Click. The handler runs count++ → the variable is now 1, and textContent updates the display.
  2. Click again → count is 2. The number on screen tracks the data.
  3. Every click repeats the cycle: change the data → update the DOM. That loop is the essence of every web app.

You just built the core of every appA to-do list, a shopping cart, a like button, a game score — all of them are "hold some data, react to an event, update the DOM." Open the Editor and build your own counter right now.

Build it in the Editor
Module 8

Creating & Removing Elements — Build a To-Do List

The counter changed an element that already existed. Real apps go further: they create brand-new elements from data and remove them on demand. Master this and you can build a to-do list, a chat, a shopping cart — anything.

Three DOM methods do it: document.createElement() makes a new element, element.append() puts it into the page, and element.remove() deletes it:

todo.js
addBtn.addEventListener("click", () => {
  const li = document.createElement("li");   // make a new element
  li.textContent = input.value;         // fill it from the input
  list.append(li);                     // add it to the page

  li.addEventListener("click", () => li.remove()); // click to delete
});

Using HTML Templates

Building complex structures with createElement over and over gets messy. The <template> tag solves this. It holds hidden HTML that the browser doesn't render, but JavaScript can clone and use it to stamp out new UI instantly.

template.html
<template id="card-template">
  <div class="card">
    <h3 class="title"></h3>
    <button class="delete-btn">Delete</button>
  </div>
</template>
script.js
const template = document.querySelector("#card-template");
// Clone the entire hidden structure
const clone = template.content.cloneNode(true); 
// Fill it with dynamic data
clone.querySelector(".title").textContent = "New Task";
document.body.append(clone);
Animated example

Adding and removing to-do items

todo.html
Buy milk
Add
Buy milk×
Walk dog×
  1. Type "Buy milk" into the input.
  2. Click AddcreateElement("li") + append() creates a brand-new item on the page.
  3. Type "Walk dog" and add it too — the list grows from data you supplied.
  4. Click the × on "Buy milk" → li.remove() deletes just that element. You've built a working to-do app.

This is a real, portfolio-worthy projectA to-do list touches everything: input, events, creating elements, removing them, and (with localStorage) even saving data between visits. It's the classic "I can actually build things now" milestone. Try it in the Editor!

Build the To-Do list in the Editor
Module 9

The Fetch API: Talking to Servers

So far your pages only work with data you typed in yourself. Real apps get their data from servers over the internet — a weather app, a feed, a product list. The tool that does this in the browser is fetch, and it uses the async skills from the JavaScript course.

What an API request looks like

An API is a URL you can ask for data. You send a request with fetch(), wait for the response (it takes time, so it's asynchronous), and turn the reply — usually JSON text — into a real JavaScript object with .json():

script.js
async function loadUser() {
  const res = await fetch("https://api.site.com/user/1");
  const user = await res.json();   // JSON text → JS object
  document.querySelector("#name").textContent = user.name;
}
Animated example

Fetching data and rendering it into the page

Your Page
GET /user/1 →
← JSON
API Server
loading…
S
Sara Ahmed
@sara · Cairo
  1. You call fetch("…/user/1"). A GET request leaves your page and travels to the API server. The card shows a loading spinner.
  2. The server finds the user and replies with JSON text — a string of key/value data.
  3. await res.json() parses that text into a real JavaScript object you can use.
  4. You write the object's fields into the DOM with textContent — and the card fills with the real data. Fetch + the DOM = a live, data-driven app.

Always handle the failure caseRequests fail — no signal, server down, bad URL. Wrap fetch in try/catch, check res.ok, and show the user a friendly "couldn't load, retry?" message instead of a blank screen. Robust apps assume the network will let them down.

Module 10

Saving Data with localStorage

Here's a problem you'll hit fast: any variable your app holds vanishes the moment the user reloads or closes the tab. To make data stick — a saved to-do list, a theme choice, a high score — the browser gives you a tiny built-in database called localStorage.

Two methods do almost everything

localStorage stores simple key/value strings directly on the user's device, and they survive reloads, tab closes, even restarts:

script.js
// SAVE a value under a key
localStorage.setItem("score", 3);

// READ it back later (even after a reload)
const saved = localStorage.getItem("score"); // "3"
Animated example

A variable is forgotten on reload — localStorage isn't

page running
Normal variable: 3
localStorage   : empty
getItem("score") → restores 3 ✓
  1. The app runs. A normal variable holds score = 3, and we also call setItem("score", 3) — writing it to localStorage on the device.
  2. The user reloads the page. The normal variable is wiped back to 0 — everything in memory is forgotten.
  3. But localStorage lives on disk, not in memory. The reload didn't touch it — it still holds "3".
  4. On load, getItem("score") reads it back and your app restores the score to 3. The data survived. That's persistence.

Two things to rememberlocalStorage only stores strings — to save an object or array, wrap it with JSON.stringify(...) on the way in and JSON.parse(...) on the way out. And never store secrets (passwords, tokens) there: any script on the page can read it. It's for convenience data, not sensitive data.

Module 11

Event Delegation & Bubbling

Here's a real problem: you have a to-do list with 200 items, each needing a click handler. Do you attach 200 listeners? What about items added later that don't exist yet? There's an elegant answer, and it relies on understanding how events actually travel.

Events bubble up

When you click a button inside a list item inside a list, the click doesn't just fire on the button. It bubbles — the event travels up through every ancestor: button → list item → list → body. Each ancestor gets a chance to react. This bubbling is the key insight most beginners never learn.

Animated example

A click bubbling up the DOM tree

<ul> one listener here 👂
<li>
<button> ✕
  1. The user moves to click the delete button inside a list item.
  2. The click fires on the <button> — the deepest element (the "target").
  3. Then it bubbles up to the parent <li>
  4. …and up to the <ul>. So we put one listener on the <ul> and catch every child's click there. That's event delegation.

Delegation: one listener to rule them all

Instead of a listener per item, you attach a single listener to the parent and check which child was actually clicked using event.target:

script.js
list.addEventListener("click", (e) => {
  if (e.target.matches(".delete-btn")) {
    e.target.closest("li").remove();   // delete that item
  }
});

Two big winsDelegation means less memory (one listener, not thousands) and — crucially — it works for elements added later. New list items you create dynamically are instantly handled, because the listener lives on the parent that was always there. (And event.stopPropagation() lets you halt bubbling when you need to.)

Module 12

Advanced: DOM Performance & Fragments

Every time you touch the DOM, the browser has to recalculate the layout and repaint the screen (called Reflow and Repaint). Doing this repeatedly inside a loop will make your app sluggish.

The DocumentFragment Solution

Instead of appending elements one by one directly to the live DOM, you can use a DocumentFragment. It's an invisible mini-DOM that exists only in memory. You append everything to it, and then append the fragment to the live DOM once.

performance.js
const list = document.querySelector("#huge-list");
const fragment = document.createDocumentFragment(); // 🧠 In-memory container

for (let i = 0; i < 1000; i++) {
  const li = document.createElement("li");
  li.textContent = `Item ${i}`;
  fragment.append(li); // ⚡ Fast: No reflow!
}

list.append(fragment); // 🎨 Only ONE reflow and repaint!

Layout ThrashingAvoid interleaving reads and writes to the DOM. If you change a style (write) then measure it with element.offsetHeight (read), you force the browser to synchronously recalculate layout on the spot. Doing this in a loop causes heavy "layout thrashing". Always read first, then write.

Module 13

Advanced: Intersection Observer API

How do you know when an element scrolls into view? The old way was listening to the scroll event, which is terrible for performance. The modern, highly optimized way is the Intersection Observer.

It acts like a camera watching elements. When an element intersects with the viewport (enters the screen), it triggers a callback. Perfect for lazy loading images, infinite scrolling, or trigger-on-scroll animations.

observer.js
const cards = document.querySelectorAll(".fade-in-card");

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add("visible"); // Animate it!
      observer.unobserve(entry.target);      // Stop watching
    }
  });
});

cards.forEach(card => observer.observe(card)); // Start watching
Module 14

Advanced: Custom Events

Built-in events (click, input, submit) are great, but as your app grows, different parts of your code need to talk to each other without being tightly coupled. You can create your own Custom Events and broadcast them across the DOM.

Imagine a Shopping Cart that updates whenever a "Product Added" event is fired from anywhere on the page, passing the product data along in the detail property.

customEvents.js
// 1. Listen for our custom event anywhere in the app
document.addEventListener("cart:add", (e) => {
  const product = e.detail; // The custom payload
  console.log(`Added ${product.name} ($${product.price})`);
});

// 2. Sometime later, dispatch the event from a button click
const buyBtn = document.querySelector("#buy-shoes");

buyBtn.addEventListener("click", () => {
  const event = new CustomEvent("cart:add", {
    detail: { name: "Nike Air", price: 120 } // Attach data here
  });
  
  document.dispatchEvent(event); // 📢 Broadcast it!
});
Module 15

Advanced: MutationObserver API

Sometimes you need to know when the DOM itself changes. Maybe a third-party script adds an ad to your page, or a dynamically loaded component finally renders. You can't just attach an event listener for DOM changes (the old ones were too slow). Enter the MutationObserver.

It efficiently watches a DOM element (like document.body or a specific container) and triggers a callback whenever nodes are added/removed, attributes change, or text is modified inside it.

mutation.js
const container = document.querySelector("#feed");

const observer = new MutationObserver((mutations) => {
  mutations.forEach(mutation => {
    if (mutation.type === "childList") {
      console.log("New elements were added or removed!");
    }
  });
});

// Start watching for changes to child elements
observer.observe(container, { childList: true });

Your pages are alive now ⚡

You can select elements, change the page, and respond to the user. That's real front-end development. The final course introduces React — the professional framework that scales this up to full applications.