JavaScriptDOM Manipulation

DOM Manipulation

Once you have an element, changing what it contains is usually a one-liner. But there are three different properties for setting text or HTML and they behave very differently — one of them is a classic source of security holes. This page covers textContent, innerText, innerHTML and insertAdjacentHTML, when each is right, and the XSS pitfall you have to keep in mind.

The element we'll change

card.html

HTML
<article id="card">
  <h2>Hello</h2>
  <p>Some text with <em>emphasis</em>.</p>
</article>
textContent — plain text, always safe

textContent reads or writes the plain text inside an element. Writing to it replaces all children with a single text node — any inner tags are flattened. Reading from it returns the concatenated text of every descendant, ignoring CSS.

JS
const card = document.getElementById("card");

card.textContent;             // "Hello\n  Some text with emphasis.\n"
card.textContent = "Replaced!";
// The <h2> and <p> are gone — card now contains a single text node.

Because the value is treated as text, you cannot accidentally inject HTML. textContent = "<b>hi</b>" literally puts the characters <b>hi</b> on the page.

innerText — text as it actually renders

innerText is close to textContent but is CSS-aware. It respects display: none, collapses whitespace the way the page does, and is therefore noticeably slower because it triggers layout.

JS
const el = document.querySelector("#card p");
el.textContent;  // exact text including hidden parts and raw whitespace
el.innerText;    // what a human sees, with line breaks and hidden text stripped
  • Use textContent when you want speed and a faithful read of the DOM.

  • Use innerText only when you specifically want "the visible rendered text" — for example to copy what the user sees.

  • For writing, prefer textContent — it never triggers layout.

innerHTML — parse a string as HTML

innerHTML reads or writes the element's markup. Writing to it tells the browser to parse the string as HTML and rebuild the element's children from that parsed tree.

JS
const card = document.getElementById("card");
card.innerHTML = "<h2>Updated</h2><p>Now with <strong>bold</strong> text.</p>";

It is convenient for static, developer-authored markup. It is also the most dangerous property in the DOM the moment user input is involved.

XSS: never put untrusted strings into innerHTML
If any part of the string came from a user — a search query, a comment, a URL parameter, anything fetched from a server — then `innerHTML` will happily parse `<script>` tags and event-handler attributes from it. An attacker who can sneak `<img src=x onerror="fetch('/steal?c='+document.cookie)">` into your page can run code in the victim's session. This class of bug is called **cross-site scripting (XSS)** and it has been the #1 web vulnerability for two decades. Default to `textContent`. Only use `innerHTML` for markup you fully control, and sanitise anything else with a library like DOMPurify.
Comparing the three on the same input

same string, three behaviours

JS
const el = document.createElement("div");

el.textContent = "<b>hi</b>";
el.innerHTML;          // "&lt;b&gt;hi&lt;/b&gt;"  — escaped, displays as text

el.innerHTML = "<b>hi</b>";
el.textContent;        // "hi"  — strips tags when read

el.innerHTML = "<img src=x onerror=alert(1)>";
// In a real page this would fire alert(1) on parse.
textContent  →  shows the characters  <b>hi</b>
innerHTML    →  parses HTML and shows  "hi" in bold
innerHTML    →  parses an <img> with an onerror handler — DANGEROUS
insertAdjacentHTML — surgical HTML insertion

insertAdjacentHTML(position, html) inserts a parsed HTML fragment next to an element instead of replacing its contents. The position is one of four strings:

positions

JS
// "beforebegin"  -> as a sibling, just before el
// "afterbegin"   -> as the first child of el
// "beforeend"    -> as the last child of el
// "afterend"     -> as a sibling, just after el

const list = document.querySelector("ul");
list.insertAdjacentHTML("beforeend", "<li>Another</li>");

Like innerHTML, this parses the string as HTML — so the same XSS rule applies. The advantage over innerHTML += "<li>Another</li>" is that the existing children are not re-parsed, so event listeners attached to them survive and performance is much better in a loop.

When to use which
  • Setting plain texttextContent. Always. Even for headings, labels, error messages.

  • Reading visible text for the userinnerText. Rarely needed.

  • Replacing markup you wrote yourselfinnerHTML is fine.

  • Appending a chunk of static markupinsertAdjacentHTML is faster and safer than innerHTML +=.

  • Anything involving user input → build with createElement + textContent, or sanitise with DOMPurify first.

A small clean rebuild

render a list safely

JS
function renderTasks(tasks) {
  const list = document.querySelector(".list");
  list.textContent = "";                     // clear safely
  for (const t of tasks) {
    const li = document.createElement("li");
    li.className = "task";
    li.textContent = t.title;                // user-supplied — treated as text
    list.append(li);
  }
}

renderTasks([
  { title: "Buy milk" },
  { title: "<img src=x onerror=alert(1)>" }, // harmless: rendered as text
]);
Mental model
`textContent` is a typewriter — it writes the characters you give it. `innerHTML` is a printing press — it takes your markup and produces formatted output. The press is more powerful, but you have to be careful about whose words you feed it.