Modifying Content
Once you’ve selected an element, the next thing you usually want to do is change what’s inside it — swap some text, render a chunk of markup, or append a new fragment without wiping out what’s already there. The DOM gives you several properties and methods for this, and choosing the right one matters: some are safe for arbitrary data, some are faster, and one of them is a notorious source of cross-site scripting (XSS) bugs. This page covers textContent, innerHTML, innerText, and insertAdjacentHTML, with a clear rule for when to reach for each.
textContent — plain text, always safe
textContent reads or writes the raw text of an element and all its descendants. When you assign to it, the browser treats the value as literal text: any < or > characters are shown as-is, never parsed as HTML. That makes it the correct default whenever you’re inserting data that could contain user input.
const title = document.querySelector("#title");
console.log(title.textContent); // read current text
title.textContent = "Hello, world!"; // replace it entirely
Because it ignores markup, assigning the string "<b>hi</b>" to textContent displays the literal characters <b>hi</b> rather than bold text. It also reads the text of hidden elements and collapses nothing — you get every character in the subtree, including from display: none nodes.
Reach for
textContentfirst. If you only need to set or read text, it is faster thaninnerHTMLand structurally immune to XSS.
innerHTML — parsed markup (handle with care)
innerHTML gets or sets the markup inside an element as an HTML string. Assigning to it tells the browser to parse the string and rebuild the element’s children from the resulting nodes. This is powerful — you can render an entire widget in one line — but it also means any HTML in the string becomes real, executable DOM.
const panel = document.querySelector("#panel");
panel.innerHTML = `
<h2>Account</h2>
<p>Signed in as <strong>Ada</strong></p>
`;
The XSS risk
The danger appears the moment the string contains data you don’t fully control. Consider rendering a username straight into innerHTML:
// DANGEROUS — never do this with untrusted input
const name = getUserInput(); // e.g. '<img src=x onerror="steal()">'
box.innerHTML = `Welcome, ${name}!`;
If name contains a tag with an event handler, the browser executes it. This is a classic stored/reflected XSS vector. The fix is simple: use textContent for the dynamic part, and reserve innerHTML for trusted, static markup only.
// SAFE — text is escaped automatically
box.textContent = `Welcome, ${name}!`;
The interactive demo below lets you type into a box and see exactly how the two properties differ when the same string is rendered both ways.
<input id="field" value="<b>not bold</b>" style="width:100%;padding:8px" />
<p>textContent: <span id="out-text"></span></p>
<p>innerHTML: <span id="out-html"></span></p>
<script>
const field = document.querySelector("#field");
const asText = document.querySelector("#out-text");
const asHtml = document.querySelector("#out-html");
function render() {
asText.textContent = field.value; // shows literal characters
asHtml.innerHTML = field.value; // parses as markup
}
field.addEventListener("input", render);
render();
</script>
innerText — what the user actually sees
innerText looks similar to textContent but is fundamentally different: it is layout-aware. It returns only the text that is rendered, respecting display: none and visibility: hidden, and it normalizes whitespace the way the page presents it. Reading innerText can even trigger a reflow, because the browser must lay the element out to know what’s visible.
const node = document.querySelector("#note");
console.log(node.textContent); // every character in the markup
console.log(node.innerText); // only the visible, rendered text
Use innerText when you specifically need the human-visible text (for example, copying what the user can read). For everything else, prefer textContent — it’s faster and predictable.
Comparing the three
| Property | Parses HTML? | Sees hidden text? | Triggers reflow? | XSS-safe to write? |
|---|---|---|---|---|
textContent | No | Yes | No | Yes |
innerHTML | Yes | Yes | No (on read) | No |
innerText | No | No | Yes (on read) | Yes |
insertAdjacentHTML — add without replacing
Setting innerHTML destroys and rebuilds all existing children, which discards their event listeners and is wasteful when you only want to add something. insertAdjacentHTML parses an HTML string and inserts it at a precise position relative to the element, leaving the current contents intact.
The first argument is one of four position keywords:
const list = document.querySelector("#list");
list.insertAdjacentHTML("beforeend", "<li>New item</li>");
<!-- beforebegin -->
<div id="list">
<!-- afterbegin -->
existing children
<!-- beforeend -->
</div>
<!-- afterend -->
This live demo appends a new card each time you click, without touching the cards already on the page:
<button id="add">Add card</button>
<div id="deck" style="display:flex;gap:8px;margin-top:12px"></div>
<script>
let count = 0;
const deck = document.querySelector("#deck");
document.querySelector("#add").addEventListener("click", () => {
count++;
deck.insertAdjacentHTML(
"beforeend",
`<div style="padding:16px;background:#6366f1;color:#fff;border-radius:8px">
Card ${count}
</div>`
);
});
</script>
insertAdjacentHTMLcarries the same XSS risk asinnerHTMLbecause it parses markup. Never pass it an interpolated, untrusted string — build elements withcreateElement/textContentinstead, or sanitize first.
Best Practices
- Default to
textContentfor setting and reading text; it’s the fastest option and immune to XSS. - Only use
innerHTMLwith trusted, static markup — never interpolate raw user input into it. - If you must render untrusted HTML, sanitize it first with a vetted library such as DOMPurify.
- Use
insertAdjacentHTMLinstead ofinnerHTML +=to add content without destroying existing nodes and listeners. - Reserve
innerTextfor the rare case where you need the visually rendered text, and remember it can force a reflow. - Build complex, data-driven nodes with
createElementandtextContentrather than string concatenation for both safety and clarity.