|$ curl https://forge-ai.dev/api/markdown?path=docs/js/dom
$cat docs/javascript-—-dom-manipulation.md
updated Recently·35 min read·published

JavaScript — DOM Manipulation

JavaScriptDOMBeginner to Advanced
Introduction

The Document Object Model (DOM) is a programming interface for web documents. It represents the page as a tree of nodes, where each node corresponds to a part of the document — elements, attributes, text content, and more. JavaScript uses the DOM to interact with and modify web pages dynamically.

When a browser loads an HTML page, it parses the HTML and constructs the DOM tree. Every HTML element becomes an element node in this tree. The DOM provides methods and properties to query, traverse, create, modify, and delete these nodes programmatically.

The DOM is not part of JavaScript itself — it is a Web API provided by the browser environment. This is why JavaScript running in Node.js does not have access to the document object or DOM methods. Understanding the DOM is essential for any client-side web developer.

dom-intro.js
JavaScript
1// The starting point — the document object
2console.log(document); // The entire HTML document
3console.log(document.title); // The page title
4console.log(document.URL); // The current URL
5console.log(document.head); // The <head> element
6console.log(document.body); // The <body> element
7
8// The DOM is a tree rooted at document
9// document → html → head + body
10// body → div → p → text
11
12// Every element is a node with properties and methods
13const body = document.body;
14console.log(body.nodeType); // 1 (ELEMENT_NODE)
15console.log(body.nodeName); // "BODY"
16console.log(body.tagName); // "BODY"
preview
The DOM Tree

The DOM tree consists of several types of nodes. The most common are element nodes, text nodes, and the document node itself. Understanding the different node types helps you navigate and manipulate the tree correctly.

Node TypenodeTypeDescriptionExample
Document9The root document objectdocument
Element1An HTML element (div, p, a, etc.)<div>
Attribute2An attribute of an element (deprecated)class="foo"
Text3Actual text content in an element"Hello"
Comment8HTML comment node<!-- comment -->
DocumentFragment11Lightweight document for batch operationsdocument.createDocumentFragment()
dom-tree.js
JavaScript
1// Visualizing the DOM tree structure
2// HTML: <div id="app"><h1>Title</h1><p>Text</p></div>
3
4const app = document.getElementById("app");
5
6// The DOM tree for the above HTML:
7// Document
8// └─ html
9// └─ body
10// └─ div#app
11// ├─ h1
12// │ └─ "Title" (text node)
13// └─ p
14// └─ "Text" (text node)
15
16// Navigating the tree
17console.log(app.childNodes); // NodeList: [h1, text("
18"), p]
19console.log(app.children); // HTMLCollection: [h1, p]
20console.log(app.firstChild); // h1 (first child node)
21console.log(app.firstElementChild); // h1 (first element child)
22console.log(app.lastChild); // text node (newline)
23console.log(app.lastElementChild); // p
24console.log(app.parentNode); // body
25console.log(app.parentElement); // body

info

Note the difference between childNodes and children. childNodes includes all node types (text nodes, comment nodes), while children only includes element nodes. Text nodes from whitespace and newlines between elements are included in childNodes.
Selecting Elements

Before you can manipulate an element, you need to select it. The DOM provides several methods for finding elements, from simple ID lookups to powerful CSS selector-based queries.

MethodReturnsPerformanceFlexibilityBest For
getElementByIdSingle elementFastestLow (ID only)Unique elements with IDs
querySelectorSingle elementFastHigh (any CSS selector)First matching element
querySelectorAllNodeList (static)FastHigh (any CSS selector)Multiple matching elements
getElementsByClassNameHTMLCollection (live)FastMedium (class name only)Live collections of elements
getElementsByTagNameHTMLCollection (live)FastLow (tag name only)All elements of a type
selecting.js
JavaScript
1// getElementById — single element by ID (fastest)
2const header = document.getElementById("header");
3console.log(header); // <header id="header">...</header>
4
5// querySelector — first match using CSS selector
6const firstButton = document.querySelector(".btn-primary");
7const navItem = document.querySelector("nav ul li:first-child");
8const form = document.querySelector("#signup-form");
9
10// querySelectorAll — all matches (static NodeList)
11const allButtons = document.querySelectorAll("button");
12const cards = document.querySelectorAll(".card.featured");
13console.log(cards.length); // number of matching elements
14
15// querySelectorAll returns a NodeList (NOT an array)
16// It has forEach but not map/filter/reduce
17cards.forEach((card) => console.log(card));
18
19// Convert NodeList to Array for full Array methods
20const cardArray = Array.from(cards);
21const cardArray2 = [...cards]; // spread works on NodeLists
22
23// getElementsByClassName — live HTMLCollection
24const items = document.getElementsByClassName("item");
25console.log(items.length); // updates automatically when DOM changes
26
27// getElementsByTagName — live HTMLCollection
28const paragraphs = document.getElementsByTagName("p");
29
30// Important: live collections reflect DOM changes in real time
31// If you remove a matching element from the DOM, it's gone from the collection
32// If you add a new matching element, it appears in the collection

best practice

Use querySelector and querySelectorAll for most cases — they accept any CSS selector, making your code more readable and maintainable. Use getElementById when you know the ID and need maximum performance. Be aware that getElementsBy* methods return live collections that update when the DOM changes, which can cause unexpected behavior if you iterate while modifying.
Traversing the DOM

Once you have a reference to an element, you can navigate to related elements using traversal properties. These allow you to move up, down, and sideways through the DOM tree.

Property/MethodDirectionReturnsDescription
parentNodeUpNode or nullParent node (any type)
parentElementUpElement or nullParent element node only
childrenDownHTMLCollectionChild elements only (no text nodes)
childNodesDownNodeListAll child nodes (including text)
firstElementChildDownElement or nullFirst child element
lastElementChildDownElement or nullLast child element
nextElementSiblingSidewaysElement or nullNext sibling element
previousElementSiblingSidewaysElement or nullPrevious sibling element
closest(selector)UpElement or nullNearest ancestor matching CSS selector
matches(selector)CheckbooleanChecks if element matches CSS selector
traversing.js
JavaScript
1// Starting point
2const activeLink = document.querySelector("nav a.active");
3
4// Traversing UP
5console.log(activeLink.parentNode); // <li> or parent node
6console.log(activeLink.parentElement); // <li> (element only)
7console.log(activeLink.closest("nav")); // <nav> — nearest matching ancestor
8console.log(activeLink.closest("li")); // <li>
9console.log(activeLink.closest("body")); // <body>
10
11// Traversing DOWN
12const nav = document.querySelector("nav");
13console.log(nav.children); // HTMLCollection of child elements
14console.log(nav.firstElementChild); // First child element
15console.log(nav.lastElementChild); // Last child element
16console.log(nav.childNodes); // All nodes including text/whitespace
17
18// Traversing SIDEWAYS
19console.log(activeLink.nextElementSibling); // Next <a> in same parent
20console.log(activeLink.previousElementSibling); // Previous <a>
21
22// Checking with matches()
23console.log(activeLink.matches("a")); // true
24console.log(activeLink.matches(".active")); // true
25console.log(activeLink.matches("nav a:first-child")); // check by position
26
27// Practical: find the section containing a specific element
28function findSection(element) {
29 return element.closest("section");
30}
31const section = findSection(activeLink);
32console.log(section?.id); // the section id
33
34// Practical: check if element is visible
35function isVisible(el) {
36 return el && el.matches(":visible") === false;
37}
🔥

pro tip

The closest() method is one of the most useful traversal tools. Given any element, it walks up the DOM tree until it finds an element matching the CSS selector. This is perfect for event delegation — you can get the parent component from a clicked button without hardcoding structure.
Manipulating Elements

Once you have selected an element, you can modify its content, attributes, and appearance. The DOM provides a complete set of properties and methods for element manipulation.

Content Manipulation

manipulating.js
JavaScript
1// textContent — plain text (safe, no HTML parsing)
2const div = document.querySelector(".content");
3div.textContent = "Hello, World!"; // Sets text
4console.log(div.textContent); // Returns text content
5
6// innerHTML — HTML content (parses HTML — be careful with user input)
7div.innerHTML = "<strong>Bold text</strong>"; // Parses HTML
8div.innerHTML += "<p>Appended paragraph</p>"; // Appends HTML (bad practice)
9
10// innerText — respects CSS visibility (slower than textContent)
11div.innerText = "Visible text"; // Respects display:none
12
13// Attribute manipulation
14const link = document.querySelector("a");
15link.href = "https://example.com";
16link.setAttribute("target", "_blank");
17link.getAttribute("href"); // "https://example.com"
18link.hasAttribute("target"); // true
19link.removeAttribute("target"); // removes the attribute
20
21// data-* attributes
22const card = document.querySelector(".card");
23card.dataset.id = "123"; // sets data-id="123"
24card.dataset.userRole = "admin"; // sets data-user-role="admin"
25console.log(card.dataset.id); // "123"
26console.log(card.getAttribute("data-user-role")); // "admin"
27
28// Style property (inline styles)
29const box = document.querySelector(".box");
30box.style.color = "#00FF41";
31box.style.backgroundColor = "#0A0A0A";
32box.style.fontSize = "14px";
33// Note: CSS property names become camelCase in JavaScript

Class Manipulation

classlist.js
JavaScript
1// className — replaces all classes (avoid, use classList)
2const el = document.querySelector(".card");
3console.log(el.className); // "card featured"
4
5// classList — modern API (recommended)
6el.classList.add("highlighted"); // Add a class
7el.classList.remove("featured"); // Remove a class
8el.classList.toggle("expanded"); // Toggle on/off
9el.classList.replace("old-class", "new-class"); // Replace
10console.log(el.classList.contains("card")); // true — check existence
11
12// Multiple classes at once
13el.classList.add("is-active", "is-visible");
14el.classList.remove("hidden", "collapsed");
15
16// Practical: toggle visibility
17function toggleVisibility(element) {
18 element.classList.toggle("hidden");
19}
20
21// Practical: conditional class
22function setActive(element, isActive) {
23 element.classList.toggle("active", isActive);
24}

warning

Never use innerHTML with user-generated content. If you set innerHTML to a string that contains user input, you open your application to Cross-Site Scripting (XSS) attacks. Always use textContent for text and createElement with proper sanitization for complex content.
Creating & Removing Elements

Creating new elements and removing existing ones is a fundamental DOM operation. Modern DOM APIs have simplified these tasks significantly compared to older approaches.

Creating Elements

creating.js
JavaScript
1// Create a new element
2const newDiv = document.createElement("div");
3newDiv.textContent = "I was created dynamically!";
4newDiv.className = "dynamic-card";
5
6// Set attributes during creation
7const link = document.createElement("a");
8link.href = "https://example.com";
9link.textContent = "Visit Example";
10link.target = "_blank";
11
12// Create text node (usually implicit via textContent)
13const textNode = document.createTextNode("Some text");
14
15// Clone an existing element
16const original = document.querySelector(".card");
17const clone = original.cloneNode(true); // true = deep clone (includes children)

Inserting Elements

inserting.js
JavaScript
1// appendChild — adds as last child
2const list = document.querySelector("ul");
3const newItem = document.createElement("li");
4newItem.textContent = "New item";
5list.appendChild(newItem);
6
7// insertBefore — inserts before a reference node
8const firstItem = list.firstElementChild;
9const anotherItem = document.createElement("li");
10anotherItem.textContent = "Inserted first";
11list.insertBefore(anotherItem, firstItem);
12
13// Modern append/prepend (works with multiple arguments and strings)
14const list2 = document.querySelector(".list");
15list2.append("Text node", document.createElement("span")); // after last child
16list2.prepend(document.createElement("i")); // before first child
17
18// Modern before/after/insertAdjacentElement
19const ref = document.querySelector(".ref-element");
20ref.before(document.createElement("hr")); // inserts before the element
21ref.after(document.createElement("hr")); // inserts after the element
22ref.replaceWith(document.createElement("div")); // replaces the element
23
24// beforebegin, afterbegin, beforeend, afterend
25ref.insertAdjacentHTML("afterend", "<p>After the element</p>");

Removing Elements

removing.js
JavaScript
1// remove() — modern, direct (recommended)
2const element = document.querySelector(".to-remove");
3element.remove();
4
5// removeChild — older approach
6const parent = document.querySelector(".container");
7const child = parent.querySelector(".to-remove");
8parent.removeChild(child);
9
10// Replace a child
11const oldChild = parent.querySelector(".old");
12const newChild = document.createElement("div");
13newChild.textContent = "Replacement";
14parent.replaceChild(newChild, oldChild);
15
16// Clear all children
17function clearElement(el) {
18 while (el.firstChild) {
19 el.removeChild(el.firstChild);
20 }
21}
22
23// Alternative: replace innerHTML (simpler but less efficient)
24element.innerHTML = "";
25
26// Alternative: replaceChildren (modern)
27element.replaceChildren(); // removes all children
28element.replaceChildren(newDiv, newSpan); // replaces with new children
preview

info

The modern element.remove() method is simpler than the older parentNode.removeChild(child) pattern. For batch operations, use replaceChildren() to replace all children at once. These modern methods are supported in all current browsers.
Styling Elements

JavaScript can manipulate CSS styles in several ways: through the style property, by managing CSS classes, or by reading computed styles. Each approach has appropriate use cases.

Inline Styles via style Property

inline-styles.js
JavaScript
1// Setting inline styles (camelCase property names)
2const box = document.querySelector(".box");
3box.style.color = "#00FF41";
4box.style.backgroundColor = "#0A0A0A";
5box.style.borderRadius = "8px";
6box.style.padding = "16px";
7
8// CSS custom properties (CSS variables)
9box.style.setProperty("--brand", "#00FF41");
10box.style.setProperty("--spacing", "1.5rem");
11
12// Reading inline styles
13console.log(box.style.color); // "#00FF41"
14console.log(box.style.getPropertyValue("--brand")); // "#00FF41"
15
16// Remove a style
17box.style.removeProperty("color"); // removes inline color
18// Setting to empty string also works
19box.style.backgroundColor = "";
20
21// Multiple styles at once (use cssText for bulk)
22box.style.cssText = "color: #00FF41; background: #0A0A0A; padding: 16px;";
23// Warning: cssText overwrites ALL inline styles
24
25// Better: use Object.assign for multiple properties
26Object.assign(box.style, {
27 color: "#00FF41",
28 backgroundColor: "#0A0A0A",
29 padding: "16px",
30 borderRadius: "8px",
31});

Computed Styles

computed-styles.js
JavaScript
1// getComputedStyle — read the FINAL applied style (from any source)
2const box = document.querySelector(".box");
3const computed = getComputedStyle(box);
4
5console.log(computed.color); // "rgb(0, 255, 65)" (always RGB)
6console.log(computed.fontSize); // "14px" (resolved value)
7console.log(computed.backgroundColor); // "rgb(10, 10, 10)"
8
9// Read CSS custom properties via computed style
10const brand = computed.getPropertyValue("--brand");
11console.log(brand); // "#00FF41" or whatever is computed
12
13// getComputedStyle triggers a synchronous reflow — cache if needed
14const styles = getComputedStyle(element);
15// Use the cached styles object multiple times
16
17// Practical: check if element is visible
18function isVisible(element) {
19 const style = getComputedStyle(element);
20 return style.display !== "none" && style.visibility !== "hidden";
21}
22
23// Practical: get element dimensions
24const rect = box.getBoundingClientRect();
25console.log({
26 top: rect.top,
27 left: rect.left,
28 width: rect.width,
29 height: rect.height,
30});

Class-Based Styling (Preferred)

class-styling.js
JavaScript
1// Using classes is better than inline styles for maintainability
2
3// Add/remove classes
4element.classList.add("is-active");
5element.classList.remove("is-loading");
6element.classList.toggle("expanded");
7
8// Toggle with a boolean condition
9element.classList.toggle("dark-mode", isDarkModeEnabled);
10
11// Replace one class with another
12element.classList.replace("btn-primary", "btn-secondary");
13
14// Check class existence
15if (element.classList.contains("active")) {
16 console.log("Element is active");
17}
18
19// Practical: theme toggle
20function toggleTheme() {
21 document.body.classList.toggle("dark-theme");
22 const isDark = document.body.classList.contains("dark-theme");
23 localStorage.setItem("theme", isDark ? "dark" : "light");
24}
25
26// Practical: media query listener
27const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
28function handleColorScheme(e) {
29 document.body.classList.toggle("dark-theme", e.matches);
30}
31mediaQuery.addEventListener("change", handleColorScheme);

best practice

Prefer class-based styling over inline styles whenever possible. Inline styles have the highest specificity and cannot be overridden by external CSS without !important. Use classes to separate concerns: JavaScript manages state (via classes), CSS defines the visual output.
DOM Performance

DOM operations are among the most expensive operations in client-side JavaScript. Every time you read or change the DOM, the browser may need to recalculate styles, reflow layout, and repaint. Understanding performance patterns is crucial for building smooth, responsive applications.

DocumentFragment

A DocumentFragment is a lightweight container that exists outside the live DOM tree. You can build an entire subtree in a fragment and append it to the DOM in a single operation, triggering only one reflow instead of one per element.

document-fragment.js
JavaScript
1// Inefficient — one reflow per appendChild
2const list = document.querySelector("ul");
3for (let i = 0; i < 1000; i++) {
4 const li = document.createElement("li");
5 li.textContent = `Item ${i}`;
6 list.appendChild(li); // 1000 reflows!
7}
8
9// Efficient — single reflow with DocumentFragment
10const fragment = document.createDocumentFragment();
11for (let i = 0; i < 1000; i++) {
12 const li = document.createElement("li");
13 li.textContent = `Item ${i}`;
14 fragment.appendChild(li); // no reflow — offscreen
15}
16list.appendChild(fragment); // single reflow

Batch DOM Reads & Writes

Layout thrashing occurs when you interleave DOM reads and writes. Every read of a layout property (like offsetHeight or getBoundingClientRect) forces the browser to recalculate styles if there are pending writes. Batch reads together and writes together.

layout-thrashing.js
JavaScript
1// Layout thrashing — interleaving reads and writes
2const boxes = document.querySelectorAll(".box");
3for (const box of boxes) {
4 const width = box.offsetWidth; // read (forces layout)
5 box.style.width = width + 10 + "px"; // write (invalidates layout)
6}
7// Next iteration: read forces layout recalculation again
8
9// Batch reads, then batch writes
10const widths = [];
11for (const box of boxes) {
12 widths.push(box.offsetWidth); // all reads first
13}
14for (let i = 0; i < boxes.length; i++) {
15 boxes[i].style.width = widths[i] + 10 + "px"; // all writes after
16}
17
18// Avoid forced synchronous layouts
19// The following properties trigger layout when read:
20// offsetTop, offsetLeft, offsetWidth, offsetHeight
21// clientTop, clientLeft, clientWidth, clientHeight
22// scrollTop, scrollLeft, scrollWidth, scrollHeight
23// getComputedStyle(), getBoundingClientRect()
24// scrollX, scrollY, innerWidth, innerHeight

Performance Best Practices

Use DocumentFragment for batch element creation — avoids multiple reflows
Batch DOM reads and writes separately — prevents layout thrashing
Cache DOM selections — avoid re-querying the same elements repeatedly
Use event delegation instead of attaching listeners to many individual elements
Prefer textContent over innerHTML — textContent is safer and faster (no HTML parsing)
Use requestAnimationFrame for visual updates — synchronizes with browser paint cycle
Avoid reading layout properties (offsetWidth, getBoundingClientRect) in loops
Use CSS classes instead of inline styles — class changes are optimized by the browser
Detach elements from DOM before heavy manipulation, then re-attach
Use content-visibility: auto CSS property for off-screen content
🔥

pro tip

The single biggest DOM performance optimization: do less. Cache selections, batch updates, and use DocumentFragment. Modern frameworks like React and Vue handle many of these optimizations automatically through virtual DOM diffing — but understanding what happens under the hood helps you make better architecture decisions.
DOM Best Practices

Cache DOM Selections

Querying the DOM is relatively expensive. If you need to access the same element multiple times, store the reference in a variable.

cache-selections.js
JavaScript
1// Bad — querying the DOM repeatedly
2document.querySelector(".header").style.color = "red";
3document.querySelector(".header").classList.add("highlighted");
4document.querySelector(".header").textContent = "New Header";
5
6// Good — cache the selection
7const header = document.querySelector(".header");
8header.style.color = "red";
9header.classList.add("highlighted");
10header.textContent = "New Header";
11
12// Bad — querying inside a loop
13for (let i = 0; i < 100; i++) {
14 document.querySelector(".item").innerHTML = `<span>${i}</span>`;
15}
16
17// Good — query once, use reference
18const item = document.querySelector(".item");
19for (let i = 0; i < 100; i++) {
20 item.innerHTML = `<span>${i}</span>`;
21}
22
23// Note: if elements are dynamically added/removed, re-query as needed
24// Stale references point to elements that may no longer be in the DOM

Use Event Delegation

Instead of attaching event listeners to every individual element, attach a single listener to a common ancestor and use event.target to determine which child was interacted with. This uses fewer listeners and automatically handles dynamically added elements.

event-delegation.js
JavaScript
1// Bad — listener on every button
2document.querySelectorAll(".delete-btn").forEach((btn) => {
3 btn.addEventListener("click", handleDelete);
4});
5// New buttons added later won't have the listener
6
7// Good — event delegation on parent
8document.querySelector(".list-container").addEventListener("click", (event) => {
9 const button = event.target.closest(".delete-btn");
10 if (!button) return; // not what we're looking for
11
12 const item = button.closest(".list-item");
13 item.remove();
14});
15
16// Practical: delegated click in a data table
17document.querySelector("table").addEventListener("click", (event) => {
18 const editBtn = event.target.closest(".edit-btn");
19 const deleteBtn = event.target.closest(".delete-btn");
20 const checkbox = event.target.closest('input[type="checkbox"]');
21
22 if (editBtn) handleEdit(editBtn.dataset.id);
23 if (deleteBtn) handleDelete(deleteBtn.dataset.id);
24 if (checkbox) handleCheckbox(checkbox);
25});

Security — Avoid innerHTML with User Data

security.js
JavaScript
1// DANGEROUS — XSS vulnerability
2const userInput = "<img src=x onerror='alert("XSS")'>";
3element.innerHTML = userInput; // Executes the script!
4
5// SAFE — use textContent
6element.textContent = userInput; // Escapes HTML, displays as text
7
8// SAFE — create elements and set properties
9const img = document.createElement("img");
10img.src = userInput; // If userInput is a URL, validate it first
11
12// SAFE — sanitize before using innerHTML
13function sanitizeHTML(str) {
14 const div = document.createElement("div");
15 div.textContent = str;
16 return div.innerHTML;
17}
18
19// Even better: use a DOMPurify library for complex cases
20// import DOMPurify from "dompurify";
21// element.innerHTML = DOMPurify.sanitize(userInput);
22
23// Always validate and escape user input before inserting into DOM
24function escapeHTML(str) {
25 const div = document.createElement("div");
26 div.textContent = str;
27 return div.innerHTML;
28}

Essential DOM Guidelines

Cache DOM selections — store element references in variables for repeated use
Use event delegation — one listener on a parent instead of many on children
Never use innerHTML with user-supplied data — XSS is the #1 web security vulnerability
Use textContent instead of innerText — textContent is faster and more predictable
Prefer classList over className and inline styles — cleaner separation of concerns
Use DocumentFragment for batch element creation — single reflow instead of hundreds
Batch DOM reads and writes to prevent layout thrashing
Use closest() for robust traversal — avoids brittle parent guessing
Check element existence before manipulation — handle cases where elements aren't found
Use modern DOM APIs (append, prepend, before, after, remove) — they are cleaner and well-supported

warning

Cross-Site Scripting (XSS) is one of the most common web security vulnerabilities. Never insert untrusted data via innerHTML, outerHTML, insertAdjacentHTML, or document.write(). Always use textContent for text, proper DOM creation methods for elements, and a sanitization library like DOMPurify when you must render user-generated HTML.
$Blueprint — Engineering Documentation·Section ID: JS-02·Revision: 1.0