Changing the DOM

JavaScript · Lesson 33 of 61 7 min read Updated

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.

JavaScript
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 text

Whatever 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:

HTMLTry it yourself
<!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.

MethodWhere 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.

HTMLTry it yourself
<!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") and remove("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") returns true or false.
  • 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.

HTMLTry it yourself
<!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:

JavaScript
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 element

Custom data belongs in data-* attributes, which you read and write through dataset. The name after data- becomes a camelCase key:

JavaScript
// 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 maths

Removing elements

Call el.remove() to take an element off the page. To empty a container, call container.replaceChildren() with no arguments.

Common mistakes

  • Using innerHTML for user text. It opens the door to XSS. Use textContent.
  • 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. createElement alone shows nothing. Call append() or another insert method.
  • Overwriting classes with className. el.className = "active" removes every other class. Use classList.add("active").
  • Doing maths on dataset values. They are strings, so "1299" + 1 is "12991". Convert with Number() first.

Key points

  • textContent sets plain text and is safe; innerHTML parses HTML and can run attacker code.
  • Build elements with createElement, fill them with textContent, and insert them with append, prepend, before or after.
  • Build many elements in memory and insert them in one go.
  • Change appearance by switching classes with classList; use style only for values calculated at run time.
  • Store custom data in data-* attributes and read it with dataset, remembering values are strings.
  • Remove elements with remove() and empty a container with replaceChildren().

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
HTML
<!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?

2. You call document.createElement("li") and set its text, but nothing appears. Why?

3. An element has data-stock-count="4". How do you read it?

4. What does row.classList.toggle("fail", marks < 33) do when marks is 40?

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