DOM Manipulation
How JavaScript reads and mutates the live HTML tree - selecting nodes, changing content and classes, and creating elements safely without opening an XSS hole.
The DOM (Document Object Model) is the live object representation of the HTML page that the browser builds when it parses HTML. JavaScript reads and mutates that tree to update the UI. This lesson runs in a browser; the earlier lessons run anywhere JavaScript runs.
document.querySelector finds the first element that matches a CSS selector. querySelectorAll returns a NodeList (not a true Array) of all matching elements. Convert a NodeList to an Array with Array.from when you need array methods like .map or .filter.
// Select a single element
const heading = document.querySelector("h1");
console.log(heading.textContent); // "Hello, world"
// Select multiple elements
const items = document.querySelectorAll("ul li");
console.log(items.length); // 3 (a NodeList)
// Convert to Array to use array methods
const texts = Array.from(items).map((li) => li.textContent);
console.log(texts); // ["Item 1", "Item 2", "Item 3"]textContent reads or replaces all text inside an element. classList lets you add, remove, and toggle CSS classes without touching the full className string. dataset exposes data-* attributes as a plain object.
const card = document.querySelector(".card");
// Read and write text
console.log(card.textContent); // "Original text"
card.textContent = "Updated text";
// Toggle a CSS class
card.classList.add("is-active");
card.classList.remove("is-hidden");
card.classList.toggle("is-highlighted"); // adds if absent, removes if present
console.log(card.classList.contains("is-active")); // true
// Access data attributes (data-user-id="42" in HTML)
console.log(card.dataset.userId); // "42"
card.dataset.userId = "99"; // sets data-user-id="99"document.createElement creates a new element in memory. .append inserts it into the DOM. .remove takes an element out. This is the safe way to add dynamic content - unlike innerHTML, it never interprets the content as markup.
const list = document.querySelector("ul");
// Create a new list item and insert it
const newItem = document.createElement("li");
newItem.textContent = "New item"; // safe: treated as text, not HTML
list.append(newItem);
// Insert before an existing element
const firstItem = list.querySelector("li");
list.insertBefore(newItem, firstItem);
// Remove an element
firstItem.remove();
// DANGER: innerHTML with user data is XSS
// list.innerHTML = userInput; // never do this
// SAFE alternative:
const safe = document.createElement("li");
safe.textContent = userInput; // treated as plain text
list.append(safe);In production
innerHTML with any user-supplied data is XSS (cross-site scripting) by default. A string like <img src=x onerror="stealCookies()"> becomes a live script the moment it hits innerHTML. Prefer textContent for plain text and createElement + append for structure. In hot loops (like rendering a long list on scroll), cache the node reference outside the loop. querySelector runs a full CSS-selector match every call and is not free.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free