Two properties set the content of an element. They look similar, and they differ in one crucial way.
textContent | innerHTML | |
|---|---|---|
| Treats the value as | plain text | HTML (parsed) |
"<b>hi</b>" shows as | the literal characters <b>hi</b> | bold “hi” |
| Safe with untrusted data | yes | no |
See the difference
<p id="a"></p>
<p id="b"></p>
<script>
const text = "<strong>Hello</strong> <em>world</em>";
document.querySelector("#a").textContent = text; // shown as text
document.querySelector("#b").innerHTML = text; // parsed as HTML
console.log(document.querySelector("#a").children.length); // → 0 (no elements created)
console.log(document.querySelector("#b").children.length); // → 2 (two elements created)
</script>
textContent created no elements. innerHTML created real <strong> and <em> elements. If the string came from a user, those could be elements they chose, including ones that run code.
The rule
> Use textContent for anything that is text. Use innerHTML only with markup you wrote yourself.
// A visitor's name: text
greeting.textContent = "Hello, " + name;
// NOT: greeting.innerHTML = "Hello, " + name;
Building a list safely
Create elements, set text, append.
<ul id="people"></ul>
<script>
const people = ["Maya", "Arjun", "<img src=x> not an image"];
for (const person of people) {
const li = document.createElement("li");
li.textContent = person;
document.querySelector("#people").append(li);
}
</script>
The last entry appears as text. No image is created.
When you need HTML structure with data
Use a <template> and fill in text with textContent:
const row = template.content.cloneNode(true);
row.querySelector(".name").textContent = user.name;
Escaping as a last resort
If you really must build an HTML string, escape the dynamic parts, replacing &, <, >, " and '.
function escapeHTML(text) {
return String(text)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
console.log(escapeHTML("<script>alert(1)</script>"));
// → <script>alert(1)</script>
Correct escaping depends on where the value goes (element text, attribute, URL, script), and it is easy to get wrong. Prefer DOM methods that avoid the problem.
Other risky spots
These also parse HTML: outerHTML, insertAdjacentHTML, document.write, DOMParser (when you insert the result), and setting srcdoc. Treat them like innerHTML.
If you must allow user HTML
For a rich text comment box, use a sanitiser library (DOMPurify is a well-known one) that parses the HTML and removes anything dangerous, then insert the cleaned result. Keep it updated, and still enforce rules on the server. A newer built-in Sanitizer API is being developed. Check its support before relying on it.
Performance note
textContent is also faster, because the browser does not need to parse anything.