JavaScript DOM Manipulation: 10 Techniques for 2026
[rank_math_toc]
JavaScript DOM manipulation is where your code meets the user’s screen. Every button that changes text, every list that adds items, every theme toggle that swaps classes—it all happens through DOM modification. Most tutorials teach you innerHTML and stop there. This lesson goes far deeper, covering the security implications of each approach, the performance characteristics that matter in production, and the patterns that senior developers actually use.
innerHTML vs textContent vs innerText
Three properties, three different behaviors. Getting this wrong creates XSS vulnerabilities:
const div = document.querySelector("#output");
// innerHTML — parses and renders HTML
div.innerHTML = "<strong>Bold text</strong>";
// Renders: Bold text (actually bold)
// textContent — sets raw text, ignores HTML
div.textContent = "<strong>Bold text</strong>";
// Renders: <strong>Bold text</strong> (visible tags)
// innerText — like textContent but respects CSS visibility
div.innerText = "Same as textContent for setting";
The Security Problem With innerHTML
innerHTML parses HTML, which means user input can inject malicious code:
// DANGEROUS — XSS vulnerability!
const userInput = '<img src=x onerror="alert(document.cookie)">';
div.innerHTML = userInput; // Executes the attack!
// SAFE — use textContent for user-generated content
div.textContent = userInput; // Displays as harmless text
Rule of thumb: Use textContent for user-provided data. Use innerHTML only for trusted HTML that you control. If you must insert HTML with dynamic values, use createElement and set properties individually.
Performance: textContent vs innerHTML
Setting innerHTML triggers the browser’s HTML parser, rebuilds the DOM subtree, and recalculates styles. textContent simply replaces the text node—much faster. For large lists or frequent updates, the difference is measurable.
// Slow: innerHTML in a loop
const list = document.getElementById("list");
for (let i = 0; i < 1000; i++) {
list.innerHTML += `<li>Item ${i}</li>`; // re-parses entire HTML each time!
}
// Fast: createElement + appendChild
const list = document.getElementById("list");
for (let i = 0; i < 1000; i++) {
const li = document.createElement("li");
li.textContent = `Item ${i}`;
list.appendChild(li);
}
For a refresher on how loops work in JavaScript, check our earlier lesson.
classList API — The Modern Way to Manage Classes
The classList API replaced the old className string manipulation approach. It's cleaner, safer, and more expressive:
const card = document.querySelector(".card");
// Add one or more classes
card.classList.add("active");
card.classList.add("highlighted", "visible"); // multiple at once
// Remove classes
card.classList.remove("hidden");
card.classList.remove("old", "deprecated"); // multiple at once
// Toggle — adds if missing, removes if present
card.classList.toggle("dark-mode");
// Toggle with force parameter
card.classList.toggle("active", true); // always adds
card.classList.toggle("active", false); // always removes
// Check if class exists
if (card.classList.contains("active")) {
console.log("Card is active");
}
// Replace one class with another
card.classList.replace("old-style", "new-style");
// Returns true if "old-style" was found and replaced
Real-World: Theme Toggle
const themeBtn = document.getElementById("theme-toggle");
themeBtn.addEventListener("click", () => {
document.body.classList.toggle("dark-theme");
const isDark = document.body.classList.contains("dark-theme");
themeBtn.textContent = isDark ? "Light Mode" : "Dark Mode";
localStorage.setItem("theme", isDark ? "dark" : "light");
});
// Restore theme on page load
if (localStorage.getItem("theme") === "dark") {
document.body.classList.add("dark-theme");
}
Always prefer classList methods over directly manipulating className. Setting className replaces ALL classes, while classList.add() and classList.remove() are surgical.
Inline Styles vs CSS Classes
The style property sets inline styles. CSS classes set styles through stylesheets. Use the right one:
const box = document.querySelector(".box");
// Inline styles — use for dynamic, computed values
box.style.transform = `translateX(${position}px)`;
box.style.backgroundColor = "#ff0000";
box.style.display = "none";
// Reading computed styles (includes stylesheet rules)
const computed = getComputedStyle(box);
console.log(computed.width); // "200px"
console.log(computed.display); // "block"
// IMPORTANT: style only reads INLINE styles
console.log(box.style.width); // "" (empty if set via CSS)
console.log(computed.width); // "200px" (actual computed value)
When to use inline styles: Animations, dynamic positioning, values calculated at runtime (scroll positions, mouse coordinates).
When to use CSS classes: Everything else. Toggle states, themes, responsive changes, visual variants. Classes are more maintainable, cacheable, and performant.
Attributes and Data Attributes
HTML attributes are managed through getAttribute/setAttribute, and custom data attributes through the dataset property:
const link = document.querySelector("a.external");
// Standard attributes
link.getAttribute("href"); // read
link.setAttribute("href", "/new-url"); // write
link.removeAttribute("target"); // remove
link.hasAttribute("rel"); // check
// Data attributes — the dataset API
// HTML: <div data-user-id="42" data-role="admin">
const div = document.querySelector("[data-user-id]");
div.dataset.userId; // "42" (camelCase conversion)
div.dataset.role; // "admin"
div.dataset.newProp = "val"; // sets data-new-prop="val"
delete div.dataset.role; // removes data-role attribute
// Use data attributes for storing metadata on elements
document.querySelectorAll("[data-action]").forEach(btn => {
btn.addEventListener("click", (e) => {
const action = e.target.dataset.action;
console.log("Action triggered:", action);
});
});
Data attributes are perfect for connecting JavaScript behavior to HTML elements without relying on class names or IDs. For more on how to read these elements, see our Selecting Elements lesson.
Creating and Inserting Elements
Building elements programmatically is the safest and most flexible approach to DOM manipulation:
// createElement + appendChild (classic)
const card = document.createElement("div");
card.className = "card";
card.id = "card-1";
const title = document.createElement("h3");
title.textContent = "New Card";
const body = document.createElement("p");
body.textContent = "Card content goes here";
card.appendChild(title);
card.appendChild(body);
document.getElementById("container").appendChild(card);
Modern Insertion Methods
The newer append(), prepend(), before(), after(), and replaceWith() methods are more versatile than the old appendChild/insertBefore:
const container = document.getElementById("list");
// append — adds to end (accepts multiple nodes AND strings)
container.append(element1, element2, "plain text");
// prepend — adds to beginning
container.prepend(newFirstItem);
// before/after — insert as siblings
const ref = document.querySelector(".current");
ref.before(newPrevSibling); // insert before ref
ref.after(newNextSibling); // insert after ref
// replaceWith — replace element entirely
ref.replaceWith(newElement);
// remove — remove element from DOM
ref.remove();
// Old way vs new way comparison:
// Old: parent.insertBefore(newNode, referenceNode)
// New: referenceNode.before(newNode)
// Old: parent.removeChild(child)
// New: child.remove()
DocumentFragment — Batch DOM Updates
When you need to add many elements at once, DocumentFragment prevents layout thrashing by batching all insertions into a single DOM operation:
// Without fragment — triggers reflow on each appendChild
const list = document.getElementById("results");
for (let i = 0; i < 500; i++) {
const li = document.createElement("li");
li.textContent = `Result ${i}`;
list.appendChild(li); // reflow each time!
}
// With fragment — single reflow at the end
const list = document.getElementById("results");
const fragment = document.createDocumentFragment();
for (let i = 0; i < 500; i++) {
const li = document.createElement("li");
li.textContent = `Result ${i}`;
fragment.appendChild(li); // no reflow — fragment is in memory
}
list.appendChild(fragment); // single reflow for all 500 items
For 10-50 elements, the difference is negligible. For hundreds or thousands, DocumentFragment is significantly faster. If you're working with arrays of data to render, fragments should be your default.
insertAdjacentHTML and insertAdjacentElement
These methods give you precise control over where HTML is inserted relative to an element:
const card = document.querySelector(".card");
// Four positions:
card.insertAdjacentHTML("beforebegin", "<div>Before card</div>");
card.insertAdjacentHTML("afterbegin", "<p>First child</p>");
card.insertAdjacentHTML("beforeend", "<p>Last child</p>");
card.insertAdjacentHTML("afterend", "<div>After card</div>");
// Result:
// <div>Before card</div> ← beforebegin
// <div class="card">
// <p>First child</p> ← afterbegin
// ... existing content ...
// <p>Last child</p> ← beforeend
// </div>
// <div>After card</div> ← afterend
insertAdjacentHTML is faster than innerHTML += because it doesn't re-parse existing content. Use it when you need to inject HTML strings at specific positions.
Template Literals for HTML Generation
Combining template literals with DOM methods creates readable, maintainable HTML generation:
function createCard(data) {
const html = `
<div class="card" data-id="${data.id}">
<h3 class="card-title">${escapeHTML(data.title)}</h3>
<p class="card-body">${escapeHTML(data.body)}</p>
<footer>
<span class="card-date">${data.date}</span>
<button class="card-delete" data-id="${data.id}">Delete</button>
</footer>
</div>
`;
return html;
}
// Always escape user-provided content!
function escapeHTML(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
// Render a list of cards
function renderCards(cards, container) {
const fragment = document.createDocumentFragment();
const temp = document.createElement("div");
cards.forEach(card => {
temp.innerHTML = createCard(card);
fragment.appendChild(temp.firstElementChild);
});
container.innerHTML = ""; // clear existing
container.appendChild(fragment);
}
Cloning Elements
Sometimes it's easier to clone an existing element than build one from scratch:
const template = document.querySelector(".card-template");
// Shallow clone — element only, no children
const shallow = template.cloneNode(false);
// Deep clone — element + all descendants
const deep = template.cloneNode(true);
// Modify the clone
deep.querySelector(".card-title").textContent = "New Title";
deep.classList.remove("card-template");
deep.classList.add("card");
document.getElementById("container").appendChild(deep);
The HTML <template> element is purpose-built for this pattern—it holds markup that isn't rendered until you clone it:
// HTML: <template id="row-template">
// <tr><td class="name"></td><td class="email"></td></tr>
// </template>
const template = document.getElementById("row-template");
const tbody = document.querySelector("tbody");
users.forEach(user => {
const row = template.content.cloneNode(true);
row.querySelector(".name").textContent = user.name;
row.querySelector(".email").textContent = user.email;
tbody.appendChild(row);
});
Performance Patterns for DOM Manipulation
DOM operations are expensive because they can trigger layout recalculations (reflows) and screen repaints. Follow these patterns:
// BAD: Reading and writing interleaved (layout thrashing)
elements.forEach(el => {
const height = el.offsetHeight; // read — triggers layout
el.style.height = height * 2 + "px"; // write — invalidates layout
// Next iteration reads again, forcing another layout calc
});
// GOOD: Batch reads, then batch writes
const heights = elements.map(el => el.offsetHeight); // all reads first
elements.forEach((el, i) => {
el.style.height = heights[i] * 2 + "px"; // all writes second
});
// GOOD: Use requestAnimationFrame for visual updates
function updatePositions() {
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.transform = `translateY(${i * 50}px)`;
});
});
}
The browser's rendering pipeline works best when you separate read operations from write operations. The MDN guide on browser rendering explains this in detail.
Practical Exercise: Dynamic Todo List
function createTodoApp(container) {
container.innerHTML = `
<input type="text" class="todo-input" placeholder="Add a task...">
<button class="todo-add">Add</button>
<ul class="todo-list"></ul>
`;
const input = container.querySelector(".todo-input");
const addBtn = container.querySelector(".todo-add");
const list = container.querySelector(".todo-list");
function addTodo(text) {
if (!text.trim()) return;
const li = document.createElement("li");
li.className = "todo-item";
li.dataset.completed = "false";
const span = document.createElement("span");
span.textContent = text;
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "Delete";
deleteBtn.className = "todo-delete";
li.append(span, deleteBtn);
list.appendChild(li);
input.value = "";
input.focus();
}
addBtn.addEventListener("click", () => addTodo(input.value));
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") addTodo(input.value);
});
// Event delegation for dynamic elements
list.addEventListener("click", (e) => {
if (e.target.classList.contains("todo-delete")) {
e.target.closest(".todo-item").remove();
} else if (e.target.closest(".todo-item")) {
const item = e.target.closest(".todo-item");
const done = item.dataset.completed === "true";
item.dataset.completed = String(!done);
item.classList.toggle("completed", !done);
}
});
}
This example brings together createElement, classList, dataset, event delegation, and the remove() method. You'll learn more about events in the Events lesson.
Key Takeaways
- Use
textContentfor user input (XSS-safe),innerHTMLonly for trusted HTML classListis the modern way to manage CSS classes—useadd(),remove(),toggle(),contains()- Prefer
createElement+appendChildoverinnerHTML +=in loops DocumentFragmentbatches DOM insertions for better performance- Modern methods (
append,prepend,before,after,remove) are cleaner than legacy equivalents insertAdjacentHTMLis faster thaninnerHTML +=for adding HTML strings- Separate DOM reads from writes to avoid layout thrashing
- Use the
<template>element andcloneNode(true)for repeated structures
Now that you can select and modify elements, it's time to make them interactive. Head to the Events & Event Listeners lesson to learn how to respond to user actions.
External Resources:
- MDN — innerHTML
- MDN — classList
- javascript.info — Modifying the Document
- MDN — DocumentFragment
- web.dev — DOM Size Best Practices
- Layout Thrashing — What Forces Reflow