DOM Manipulation
DOM Manipulation
The Document Object Model (DOM) is the browser's live, in-memory representation of your page: every tag becomes an object you can read and change from JavaScript. Change the objects and the page updates instantly — that is all "dynamic" web pages are. This tutorial covers selecting, creating, modifying, and removing elements, plus the traps that catch beginners.
Step 1: Selecting Elements
// Modern, flexible — accepts any CSS selector
const header = document.querySelector("#main-title");
const firstCard = document.querySelector(".card");
const allCards = document.querySelectorAll(".card");
// Older but still common
const byId = document.getElementById("main-title");
const byClass = document.getElementsByClassName("card");
Why querySelector is the usual choice: it takes full CSS selectors (".sidebar li.active" works), so one API covers everything. Two details matter. First, querySelector returns null when nothing matches — always check before using the result. Second, querySelectorAll returns a static NodeList (a snapshot), while getElementsByClassName returns a live HTMLCollection that changes as the page changes. Live collections shrink underneath you while looping, a classic source of confusing bugs.
Step 2: Creating and Inserting Elements
const div = document.createElement("div");
div.textContent = "Hello, World!";
div.className = "greeting";
document.body.appendChild(div);
// Modern insertion methods offer more control
const list = document.querySelector("#list");
list.append(div); // as last child
list.prepend(div); // as first child
div.after(document.createElement("hr")); // insert right after div
Why build then insert: an element created with createElement lives only in memory until you attach it. Configure it fully first, then insert once — every insertion can trigger the browser to recalculate layout, so batching work in memory is faster. For many elements, append them to a DocumentFragment and insert the fragment in a single operation.
Step 3: Modifying Elements Safely
const element = document.querySelector("#myElement");
// Content
element.textContent = "New text"; // safe: treated as plain text
element.innerHTML = "Bold"; // parses HTML — use with care
// Attributes
element.setAttribute("data-id", "123");
element.getAttribute("data-id"); // "123"
element.dataset.id; // "123" — nicer data-* access
// Classes — prefer classList over className juggling
element.classList.add("active");
element.classList.remove("active");
element.classList.toggle("active");
element.classList.contains("active");
// Inline styles
element.style.color = "red";
element.style.backgroundColor = "blue"; // CSS names become camelCase
Why textContent over innerHTML: innerHTML parses its input as HTML. If any part of that string came from a user, you have created a cross-site scripting (XSS) hole — an attacker's input becomes running code in your page. textContent cannot execute anything. Rule of thumb: innerHTML only for templates you wrote yourself, never for user data.
Step 4: Removing and Replacing
const item = document.querySelector("#toRemove");
item.remove(); // modern, direct
item.parentNode.removeChild(item); // old style, seen in legacy code
oldNode.replaceWith(newNode); // swap in place
Step 5: Traversing the Tree
const el = document.querySelector(".item");
el.parentElement; // up
el.children; // element children only
el.firstElementChild;
el.lastElementChild;
el.nextElementSibling; // across
el.previousElementSibling;
el.closest(".card"); // nearest ancestor matching a selector
Why the "Element" variants: plain childNodes and nextSibling include text nodes — even the invisible whitespace between tags counts as a node. The Element versions skip those, which is nearly always what you want. closest() is the workhorse of event delegation: from a clicked element, walk up to the component that owns it.
Step 6: Looping Over Selections
document.querySelectorAll(".card").forEach((card, i) => {
card.classList.toggle("featured", i === 0);
card.dataset.index = String(i);
});
const titles = [...document.querySelectorAll("h2")].map(h => h.textContent);
Why the conversion habit matters: a NodeList supports forEach, but not map or filter. When you need those, spread it into a real array first. This one idiom — select, convert, transform — turns the page itself into ordinary array data, so every array skill you have applies directly to the DOM.
Common Beginner Errors and Fixes
- Script runs before the HTML exists:
querySelectorreturnsnulland the next line throwsCannot read properties of null. Fix: load your script withdefer, place it at the end ofbody, or wrap code in aDOMContentLoadedlistener. - Treating NodeList like an array:
querySelectorAll(".x").map(...)fails — NodeList hasforEachbut notmap/filter. Fix: convert with[...nodeList]orArray.from(nodeList). - Looping over a live collection while removing: half the elements survive. Fix: use
querySelectorAll(static), or convert to an array first. - Assigning to className and wiping other classes:
el.className = "active"erases everything else. Fix:el.classList.add("active"). - Injecting user input with innerHTML: XSS vulnerability. Fix:
textContent, or sanitize with a library if HTML is genuinely required.
Practice Exercise
Build a mini to-do list without any HTML edits — construct everything from JavaScript:
ul element and append it to document.body.addTodo(text) that creates an li, sets its textContent, and appends it.children[1].remove()."done" on the first item and verify it with classList.contains.li a data-id attribute and log every id by looping over ul.children (remember to convert it before using array methods).Everything in this exercise — create, configure, insert, select, mutate, remove — is the exact life cycle every UI framework performs under the hood. Once it feels routine, you understand what React and Svelte are automating for you.