Changing the DOM
Selecting elements is only half the job. Real pages change all the time: a cart count goes up, a new message appears, a sold-out product turns grey. In this lesson you will change text, build new elements from data, switch CSS classes, work with attributes and data-* values, and remove elements.
You will also learn the single most important safety rule of front-end JavaScript: use textContent, not innerHTML, for text. You should know how to select elements from The DOM: Selecting Elements.
Changing text with textContent
Every element has a textContent property. Read it to get the text inside; assign to it to replace everything inside the element with plain text.
const heading = document.querySelector("h1");
heading.textContent = "Today's offers"; // replaces the text
const count = document.querySelector("#cart-count");
count.textContent = Number(count.textContent) + 1; // numbers are converted to textWhatever you assign is shown exactly as written. If the string contains <b>, the visitor sees the characters <b> on the screen. That might sound like a limitation, but it is exactly what keeps your page safe.
textContent vs innerHTML
innerHTML also replaces an element's contents, but it parses the string as HTML. Tags become real elements, and some attributes can run code. That is dangerous whenever the string comes from a user, a URL, a database or an API.
Run this example. The review text pretends to come from a visitor:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>textContent vs innerHTML</title>
</head>
<body>
<h1>Latest review</h1>
<p>Safe (textContent): <span id="safe"></span></p>
<p>Unsafe (innerHTML): <span id="unsafe"></span></p>
<script>
// Imagine this came from a form that any visitor can fill in.
const review = 'Great samosas! <img src="x" onerror="document.body.style.background = \'red\'">';
document.querySelector("#safe").textContent = review; // shown as plain text
document.querySelector("#unsafe").innerHTML = review; // parsed as HTML: the code runs
</script>
</body>
</html>The page turns red. The <img> failed to load, so the browser ran its onerror code. Changing a colour is harmless, but an attacker could just as easily read the visitor's data or send requests as them. This attack is called cross-site scripting (XSS), and innerHTML with untrusted text is its most common cause.
Creating and adding elements
To add new content safely, create elements with document.createElement(), fill them with textContent, then place them on the page. Creating an element does not show it anywhere: it exists only in memory until you insert it.
| Method | Where the new content goes |
|---|---|
parent.append(a, b) | At the end, inside parent |
parent.prepend(a) | At the start, inside parent |
el.before(a) | Just before el, outside it |
el.after(a) | Just after el, outside it |
el.replaceWith(a) | In place of el |
parent.replaceChildren(a, b) | Removes all children, then adds these |
All of these accept elements and plain strings, and a string is always added as text, never as HTML.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Building a list</title>
<style>
.out-of-stock { color: gray; text-decoration: line-through; }
</style>
</head>
<body>
<h1>Rohan's Sports Shop</h1>
<ul id="products"></ul>
<script>
const products = [
{ name: "Cricket bat", price: 2499, inStock: true },
{ name: "Football", price: 899, inStock: false },
{ name: "Badminton racket", price: 1299, inStock: true },
];
const list = document.querySelector("#products");
const items = products.map((product) => {
const li = document.createElement("li");
li.textContent = `${product.name}: ₹${product.price.toLocaleString("en-IN")}`;
li.dataset.price = product.price;
if (!product.inStock) {
li.classList.add("out-of-stock");
}
return li;
});
list.append(...items);
const note = document.createElement("p");
note.textContent = `${items.length} products listed.`;
list.after(note);
</script>
</body>
</html>The script builds every <li> in memory first, then adds them all with one append(...items) call. Inserting once is faster than touching the live page inside a loop, because the browser has less layout work to redo. This "data in, elements out" pattern is how most interfaces are built, including the to-do app project at the end of this course.
Changing classes and styles
The best way to change how something looks is to switch a CSS class and let the stylesheet do the work. Every element has a classList with handy methods:
add("a")andremove("a")add or remove a class.toggle("a")flips it;toggle("a", condition)adds it when the condition is true and removes it when false.contains("a")returnstrueorfalse.replace("old", "new")swaps one class for another.
For values that change continuously, such as a width calculated from data, set the style property directly. Property names use camelCase: background-color in CSS becomes style.backgroundColor in JavaScript.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Classes and styles</title>
<style>
.row { display: flex; gap: 1rem; align-items: center; margin: 0.4rem 0; }
.name { inline-size: 6rem; }
.track { inline-size: 12rem; block-size: 0.8rem; background: #eee; border-radius: 4px; }
.bar { block-size: 100%; border-radius: 4px; background: steelblue; }
.fail .bar { background: crimson; }
.fail .name { font-weight: bold; }
</style>
</head>
<body>
<h1>Maths results</h1>
<div id="results"></div>
<script>
const students = [
{ name: "Aarav", marks: 78 },
{ name: "Meera", marks: 92 },
{ name: "Kabir", marks: 29 },
];
const results = document.querySelector("#results");
for (const student of students) {
const row = document.createElement("div");
row.classList.add("row");
row.classList.toggle("fail", student.marks < 33);
const name = document.createElement("span");
name.className = "name";
name.textContent = `${student.name} (${student.marks})`;
const track = document.createElement("div");
track.className = "track";
const bar = document.createElement("div");
bar.className = "bar";
bar.style.inlineSize = `${student.marks}%`;
track.append(bar);
row.append(name, track);
results.append(row);
}
</script>
</body>
</html>The colour rules live in CSS; JavaScript only decides which class applies. The width is different for every student, so that one value goes in style.
Attributes and data-* values
Most standard attributes are also properties: img.src, link.href, input.disabled, el.hidden. For anything else, use the attribute methods:
const button = document.querySelector("#pay");
button.setAttribute("aria-busy", "true"); // add or change an attribute
button.removeAttribute("aria-busy"); // remove it
button.toggleAttribute("disabled", true); // add when true, remove when false
button.disabled = false; // the same thing, as a property
document.querySelector("#help").hidden = true; // hide an elementCustom data belongs in data-* attributes, which you read and write through dataset. The name after data- becomes a camelCase key:
// HTML: <li data-price="1299" data-stock-count="4">Badminton racket</li>
const item = document.querySelector("li");
item.dataset.price; // "1299" (always a string)
item.dataset.stockCount; // "4" (data-stock-count becomes stockCount)
item.dataset.discount = "10"; // adds data-discount="10" to the element
Number(item.dataset.price) * 2; // 2598: convert before doing mathsRemoving elements
Call el.remove() to take an element off the page. To empty a container, call container.replaceChildren() with no arguments.
Common mistakes
- Using
innerHTMLfor user text. It opens the door to XSS. UsetextContent. - Writing
innerHTML +=inside a loop. Each time, the browser re-reads all the existing HTML and rebuilds it, which is slow and throws away any event listeners on those elements. - Creating an element and forgetting to insert it.
createElementalone shows nothing. Callappend()or another insert method. - Overwriting classes with
className.el.className = "active"removes every other class. UseclassList.add("active"). - Doing maths on
datasetvalues. They are strings, so"1299" + 1is"12991". Convert withNumber()first.
Key points
textContentsets plain text and is safe;innerHTMLparses HTML and can run attacker code.- Build elements with
createElement, fill them withtextContent, and insert them withappend,prepend,beforeorafter. - Build many elements in memory and insert them in one go.
- Change appearance by switching classes with
classList; usestyleonly for values calculated at run time. - Store custom data in
data-*attributes and read it withdataset, remembering values are strings. - Remove elements with
remove()and empty a container withreplaceChildren().
Exercise
Start with an empty <ul id="stations">. Using an array of four stations on a train route (for example Delhi, Rohtak, Jind, Bathinda), create one <li> per station with createElement and textContent. Give the first and last items the class terminus and style that class in bold. Finally, add a paragraph after the list that says how many stops there are.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Train route</title>
<style>.terminus { font-weight: bold; }</style>
</head>
<body>
<ul id="stations"></ul>
<script>
const stations = ["Delhi", "Rohtak", "Jind", "Bathinda"];
const list = document.querySelector("#stations");
const items = stations.map((name, index) => {
const li = document.createElement("li");
li.textContent = name;
li.classList.toggle("terminus", index === 0 || index === stations.length - 1);
return li;
});
list.append(...items);
const summary = document.createElement("p");
summary.textContent = `${stations.length} stops on this route.`;
list.after(summary);
</script>
</body>
</html>classList.toggle with a condition adds the class only to the first and last stations, and every item is inserted with a single append call.
Quick Quiz
1. Why is textContent safer than innerHTML for user input?
textContent never parses HTML, so a malicious <img onerror> is shown as harmless text.
2. You call document.createElement("li") and set its text, but nothing appears. Why?
A created element lives in memory until you insert it into the page.
3. An element has data-stock-count="4". How do you read it?
The part after data- becomes camelCase, so stock-count becomes stockCount.
4. What does row.classList.toggle("fail", marks < 33) do when marks is 40?
With a second argument, toggle adds the class when it is true and removes it when it is false.
Learn JavaScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
