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
<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.
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.
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 strippedUse
textContentwhen you want speed and a faithful read of the DOM.Use
innerTextonly 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.
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.
Comparing the three on the same input
same string, three behaviours
const el = document.createElement("div");
el.textContent = "<b>hi</b>";
el.innerHTML; // "<b>hi</b>" — 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
// "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 text →
textContent. Always. Even for headings, labels, error messages.Reading visible text for the user →
innerText. Rarely needed.Replacing markup you wrote yourself →
innerHTMLis fine.Appending a chunk of static markup →
insertAdjacentHTMLis faster and safer thaninnerHTML +=.Anything involving user input → build with
createElement+textContent, or sanitise with DOMPurify first.
A small clean rebuild
render a list safely
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
]);