AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

DOM Manipulation and Events: Dynamic Web Pages

📚 Web Development Foundations⏱️ 23 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 23 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Open any app on your phone and tap a heart icon on a post. The heart turns red and fills in immediately — no white flash, no spinning loader, no new page. Now do the same thing on the IRCTC website: tap "Check Availability" for a train, and the seat numbers update right there on the page while everything else — the header, the train name, the date picker — stays exactly where it was. Neither of these pages reloaded. Something inside the browser changed the page while you were looking at it.

That "something" is the subject of this chapter. It has two parts, and they always work together. First, the browser keeps a living, changeable model of the page in memory, called the DOM — the Document Object Model. Second, the page listens for things you do — clicks, taps, key presses — called events, and runs JavaScript code in response. DOM manipulation is how a program reaches into that model and edits it; events are how a program knows when to do the editing. Put them together and you get a page that reacts to you instead of just sitting there.

HTML is a recipe. The DOM is the cake.

Before touching any code, it helps to fix a confusion that trips up almost every beginner: the DOM is not the same thing as your HTML file. Your HTML file is a text document — a recipe written in tags. When the browser loads that file, it reads the recipe and builds something out of it: a tree of objects sitting in the computer's memory, each object representing one tag or one piece of text. That tree — not the text file — is the DOM. It is what the browser actually draws on your screen, and it is what JavaScript is allowed to touch and change.

Here is why the distinction matters. Suppose your page has this HTML:

<!DOCTYPE html>
<html>
<head>
  <title>My Tasks</title>
</head>
<body>
  <h1 id="main-title">My Tasks</h1>
  <ul id="task-list">
    <li>Buy milk</li>
    <li>Finish homework</li>
  </ul>
</body>
</html>

If a script later changes the heading's text to "3 Tasks Left", and then you right-click the page and choose "View Page Source", you will still see <h1 id="main-title">My Tasks</h1> — the original file, untouched. But if you right-click the heading itself and choose "Inspect", you will see <h1 id="main-title">3 Tasks Left</h1> — the live DOM, which the script actually changed. "View Source" reads the recipe from disk. "Inspect" reads the cake that was baked from it and has been getting frosted ever since the page loaded. This is exactly why the "like" button and the IRCTC seat counts can change without a reload: JavaScript edits the cake, not the recipe.

Two kinds of nodes: elements and text

The DOM tree is built from small pieces called nodes. For this chapter, two kinds matter most. An element node represents a tag, such as <h1> or <li>. A text node represents the actual words sitting inside a tag — and every piece of visible text in an HTML file becomes its own separate node, a child of the element that contains it. So a tag is a container; the words inside it are a separate thing living inside that container. Notice in the HTML above that the word "My Tasks" appears twice — once inside <title> and once inside <h1>. These are two independent text nodes in two different places in the tree, even though the words are identical; changing one never affects the other.

document <html> <head> <body> <title> <h1 id="main-title"> "My Tasks" "My Tasks" Element node (a tag) Text node (the words inside a tag)

Notice the shape: <title> and <h1> are siblings under different parents (<head> and <body>), and each one has its own text node child holding the words "My Tasks". This is the rule to remember: every tag's wording becomes its own text node, wherever that tag appears in the tree — the <ul> and its two <li> children would extend this same tree one level further, each <li> getting its own text node for "Buy milk" or "Finish homework".

Finding an element before you can change it

JavaScript cannot change what it cannot find. To edit a node, you first grab a reference to it, using methods that live on the global document object. The two you will use constantly:

const heading = document.getElementById("main-title");
console.log(heading.textContent); // "My Tasks"

const heading2 = document.querySelector("#main-title");
console.log(heading2 === heading); // true — both point to the same element

const firstTask = document.querySelector("li"); // the FIRST <li> in the whole document
console.log(firstTask.textContent); // "Buy milk"

const allTasks = document.querySelectorAll("li");
console.log(allTasks.length); // 2

getElementById takes a plain id (no #) and returns exactly one element, or null if nothing matches. querySelector is more general: it accepts any CSS selector — an id with #, a class with ., a tag name, even something like "ul li:first-child" — and returns the first matching element. querySelectorAll returns every match, as a list-like object called a NodeList, which is why allTasks.length gives the count of matching <li> elements. Trace it against the sample HTML above: main-title is the id on the <h1>, whose text node holds "My Tasks", so both lookups return that exact element and heading.textContent reads out "My Tasks" — no guessing, you can check it against the file itself.

Reading and changing what's on screen

Once you hold a reference, the most common thing to change is the text inside it. There are two properties that look similar but behave very differently, and mixing them up is one of the most common beginner mistakes:

heading.textContent = "My Tasks <em>(3 pending)</em>";
// The browser shows the LITERAL characters:
// My Tasks <em>(3 pending)</em>   -- no italics, the tags are just text

heading.innerHTML = "My Tasks <em>(3 pending)</em>";
// The browser PARSES the string as HTML and shows:
// My Tasks (3 pending)   -- with "(3 pending)" now genuinely in italics

Misconception check: many students treat textContent and innerHTML as interchangeable ways to "put text on the page." They are not. textContent always inserts a plain string of characters — if that string happens to contain < and >, they show up as literal symbols on screen, exactly like typing them into a text editor. innerHTML instead hands the string to the browser's HTML parser, which builds new tags out of anything that looks like a tag. That difference is also a safety issue: if you ever insert text that a user typed — a comment, a search box value — using innerHTML, and that user typed something like <img src=x onerror="doSomethingBad()">, the browser would actually run it. This class of bug is called XSS (cross-site scripting), and it is why the rule of thumb is: use textContent for anything that is just words, and reach for innerHTML only when you deliberately want to insert real markup you trust.

Changing appearance: style and classList

Text is not the only thing you can edit. Every element node has a .style object for direct CSS properties, and a .classList object for adding, removing, or toggling CSS classes — the more common approach in real projects, since it keeps styling rules in your CSS file instead of scattering them across JavaScript.

heading.style.color = "green";      // inline style, applied immediately

heading.classList.add("done");      // adds the class "done"
heading.classList.toggle("done");   // it's already there, so this REMOVES it
heading.classList.contains("done"); // false

toggle is worth pausing on: called once on a class that is absent, it adds the class; called again on a class that is present, it removes it. This single method is what usually powers a "mark as done" checkbox or a dark-mode switch — one line, no if statement needed to check which state you're in.

Building new elements and taking old ones away

So far every example edited a node that already existed in the HTML file. But the DOM tree is not fixed at page-load time — JavaScript can grow it or prune it while the page is running, which is exactly how a "like" count increases or a to-do item gets added without ever touching the HTML file.

const list = document.getElementById("task-list");

const newItem = document.createElement("li");
newItem.textContent = "Pack school bag";
list.appendChild(newItem);
// <ul id="task-list"> now has THREE <li> children;
// the new one is last, because appendChild adds to the end

createElement makes a brand-new element node that exists only in memory — it is not attached anywhere yet, and nothing on screen changes until you attach it. appendChild does the attaching, inserting the new node as the last child of whichever element you call it on. Removing works just as directly:

const items = document.querySelectorAll("#task-list li");
items[0].remove(); // items[0] is "Buy milk" — it disappears from the page

Because querySelectorAll returns matches in document order, items[0] is guaranteed to be the first <li> in the sample HTML — "Buy milk" — and calling .remove() on it deletes that node from the tree, which the browser immediately reflects on screen.

Events: teaching the page to respond

Everything above changes the DOM, but it all ran the instant the script loaded. A real interactive page needs to wait — sometimes for seconds, sometimes for hours — until the user does something specific, and only then run its code. That waiting-and-reacting mechanism is an event: a signal the browser fires when something happens (a click, a key press, a page finishing its load), and an event listener is a function you register to run whenever that signal fires.

const button = document.getElementById("add-btn");

button.addEventListener("click", function () {
  console.log("Button was clicked!");
});

addEventListener takes two things: the name of the event to watch for (as a string — "click", "keydown", "submit", "input" are the most common) and a function to run when it happens. That function receives one argument, conventionally called event, carrying details about what just happened — which element was actually touched, which key was pressed, and so on:

document.getElementById("task-list").addEventListener("click", function (event) {
  console.log(event.target.tagName); // the exact element the click landed on
});

event.target is not necessarily the element you attached the listener to — it is whichever element the user's click actually hit, which might be a child sitting deep inside it. That distinction turns out to matter a great deal, as the next section shows.

Misconception check: onclick versus addEventListener

Many beginners meet element.onclick = function () { ... } before addEventListener and assume they are just two spellings of the same thing. They are not, and the difference causes real bugs:

button.onclick = function () { console.log("first handler"); };
button.onclick = function () { console.log("second handler"); };
// Clicking the button now logs ONLY "second handler" —
// the second assignment silently overwrote the first.

button.addEventListener("click", function () { console.log("first handler"); });
button.addEventListener("click", function () { console.log("second handler"); });
// Clicking the button logs BOTH lines, in the order they were registered.

onclick is a single property, like any other object property — assigning to it a second time simply replaces whatever was there before. addEventListener instead maintains a list of listeners, so multiple parts of a program (say, one script that logs analytics and another that updates the UI) can each register their own handler on the same element without stepping on each other. For this reason, modern JavaScript almost always uses addEventListener.

Event bubbling: a click travels upward

Here is a question that seems to have an obvious answer but doesn't: if you click a small "✕" delete icon sitting inside a list item, which element receives the click event — just the icon, or the icon and everything around it too? The answer is: both, one after another. After the event fires on the exact element clicked, the browser walks back up the tree, from parent to grandparent to great-grandparent, firing the same event again on every ancestor, all the way up to document. This upward journey is called bubbling, by analogy with a bubble rising through water.

Picture a task list where each item includes its own delete icon:

<ul id="task-list">
  <li>Buy milk <span class="del-btn">✕</span></li>
  <li>Finish homework <span class="del-btn">✕</span></li>
</ul>

If the user clicks the inside "Buy milk", the click event fires first on that <span> (its event.target), then on its parent <li>, then on <ul>, then <body>, then <html>, then document — six firings from one physical tap, because the span sits six levels deep including itself.

Click the ✕ inside "Buy milk" — the event starts at span and bubbles outward, level by level document <html> <body> <ul id="task-list"> <li> span.del-btn (target) 6 — document 5 — html 4 — body 3 — ul#task-list 2 — li 1 — span (target) Bubbling order: 1 span(target) → 2 li → 3 ul → 4 body → 5 html → 6 document

Event delegation: one listener instead of a hundred

Bubbling looks like a curiosity until you see the problem it solves. Imagine a to-do list that can grow to fifty items, each with its own delete icon. Attaching a separate addEventListener to every single icon — including ones the user adds later — is wasteful and easy to get wrong, because a listener you attach today does nothing for an <li> that does not exist yet. The fix uses bubbling directly: attach one listener to the stable parent, and let every click inside it bubble up to that one place.

document.getElementById("task-list").addEventListener("click", function (event) {
  if (event.target.classList.contains("del-btn")) {
    event.target.parentElement.remove();
  }
});

Trace this against the diagram above. A click on the inside "Buy milk" fires on the <span class="del-btn"> first, so event.target is that span. The listener itself sits on <ul id="task-list">, three levels up — but because the event bubbles, the listener still runs, on step 3 of the journey shown in the diagram. Inside it, event.target.classList.contains("del-btn") checks whether the actual clicked element carries that class; since it does, event.target.parentElement is the enclosing <li>, and .remove() deletes that whole list item — "Buy milk" and its icon both vanish. If the user instead clicks on plain list text (not the icon), event.target would be the <li> itself, classList.contains("del-btn") would be false, and nothing would happen — exactly as intended. One listener now correctly handles every delete icon, present and future, because it never needed to know how many <li> elements exist; it only needed to know that clicks on any of them eventually bubble up to <ul>. This pattern is called event delegation, and it is the standard way production code handles lists, tables, and menus whose contents change after the page loads.

Putting it together: a working to-do list

The full picture combines selecting, reading input, creating elements, and two kinds of events — a click on a button and a keypress on a text field:

<input id="new-task" type="text" ="New task">
<button id="add-btn">Add</button>
<ul id="task-list">
  <li>Buy milk <span class="del-btn">✕</span></li>
  <li>Finish homework <span class="del-btn">✕</span></li>
</ul>

<script>
const input = document.getElementById("new-task");
const addBtn = document.getElementById("add-btn");
const list = document.getElementById("task-list");

function addTask() {
  const text = input.value.trim();
  if (text === "") return;          // ignore empty / spaces-only input

  const li = document.createElement("li");
  li.textContent = text + " ";

  const del = document.createElement("span");
  del.className = "del-btn";
  del.textContent = "✕";
  li.appendChild(del);

  list.appendChild(li);
  input.value = "";                 // clear the box for the next task
}

addBtn.addEventListener("click", addTask);

input.addEventListener("keydown", function (event) {
  if (event.key === "Enter") addTask();
});

list.addEventListener("click", function (event) {
  if (event.target.classList.contains("del-btn")) {
    event.target.parentElement.remove();
  }
});
</script>

Walk through one full interaction. Suppose a student types "Pack school bag" into the box and presses Enter. The keydown listener fires with event.key === "Enter", so it calls addTask(). Inside addTask, text becomes "Pack school bag" (trimmed of any stray spaces), which is not empty, so a new <li> is created, given the text, and handed its own span exactly like the two that were already there; that <li> is appended to list, and the input box is cleared. The list now shows three items, and — crucially — nobody had to attach a new delete listener to the third item, because the delegation pattern from the previous section already covers it: clicking its bubbles up to the same list listener as the original two. If the student now clicks the next to "Buy milk", that item is removed the same way traced earlier. At no point did the browser reload the page or re-run the HTML file — every change is a live edit to the DOM tree sitting in memory, exactly like the heart icon and the IRCTC seat count from the opening of this chapter.

Check your understanding

  • A classmate writes document.getElementByID("score") and it fails. What exactly is wrong, and how would you fix it?
  • Given the sample HTML in this chapter, what does document.querySelectorAll("li").length return, and why?
  • You want to insert the text a user typed into a search box (say, they typed "2 < 3") onto the page. Should you use textContent or innerHTML? What would go wrong with the other choice?
  • A button has two addEventListener("click", ...) calls registered on it, versus a different button with two .onclick = ... assignments. Clicking each button once — how many total handler functions actually run, and why the difference?
  • In the to-do list example, if you clicked directly on the word "Finish homework" (not the ✕), would list's click listener still run? Would anything visible happen? Explain using bubbling.
  • Why does event delegation (one listener on <ul>) still work correctly for an <li> that gets added five minutes after the page first loaded, when a listener attached directly to that specific <li> could never have existed yet?

Answers: (1) JavaScript is case-sensitive; the real method is getElementById (lowercase "d" in "Id"), so the typo throws "getElementByID is not a function". (2) It returns 2, since the sample HTML has exactly two <li> elements, "Buy milk" and "Finish homework". (3) Use textContent — it shows 2 < 3 as literal characters; innerHTML would try to parse < 3 as the start of a tag and could behave unpredictably or unsafely if the typed text ever contained real tags. (4) The addEventListener button runs both functions, because listeners stack; the onclick button runs only the second function, because each assignment overwrites the property's previous value. (5) Yes, the listener still runs, because the click still bubbles from the <li> up through <ul>; but nothing visible happens, because event.target is the <li> itself, not something carrying the del-btn class, so the if check is false. (6) Because the listener isn't attached to the <li> at all — it's attached to <ul>, which already existed before the new item was added; bubbling carries every future child's click up to that same permanent parent automatically.

Summary

  • The DOM is the browser's live, in-memory tree built from your HTML — not the HTML file itself. JavaScript edits the tree; "View Source" still shows the original file.
  • The tree is made of element nodes (tags) and text nodes (the words inside them); each tag's wording is its own separate text node, even if identical words appear elsewhere in the tree.
  • getElementById, querySelector, and querySelectorAll find nodes; textContent (literal text, safe) and innerHTML (parsed as HTML, riskier with user input) read or write what's inside them.
  • style and classList.add/remove/toggle change appearance; createElement plus appendChild grow the tree; .remove() prunes it — all while the page keeps running, with no reload.
  • An event listener, registered with addEventListener, runs a function when something happens; unlike onclick, multiple listeners can stack on the same element without overwriting each other.
  • A click bubbles: it fires first on the exact element touched (event.target), then again on every ancestor up to document. Event delegation uses this to let one listener on a stable parent correctly handle clicks on children that don't exist yet.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where dom manipulation and events: dynamic web pages is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting dom manipulation and events: dynamic web pages to at least 3 other topics you have studied.
← HTML5 and the Semantic Web: Building Meaningful PagesWeb Forms and Validation: User Input Done Right →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn