Back to Tutorials
🌐 Intermediate

DOM Manipulation

7 min read | DOM

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: querySelector returns null and the next line throws Cannot read properties of null. Fix: load your script with defer, place it at the end of body, or wrap code in a DOMContentLoaded listener.
  • Treating NodeList like an array: querySelectorAll(".x").map(...) fails — NodeList has forEach but not map/filter. Fix: convert with [...nodeList] or Array.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:

  • Create a ul element and append it to document.body.
  • Write addTodo(text) that creates an li, sets its textContent, and appends it.
  • Add three todos, then remove the middle one using children[1].remove().
  • Toggle a class "done" on the first item and verify it with classList.contains.
  • Bonus: give each 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.