skip to content

What is the difference between setting an element's textContent and setting its innerHTML in the browser DOM, and when does that difference cause a bug?

level: juniorimportance: must knowfreq 78%

answer

  1. text versus markup
  2. one of them parses, one does not
  3. angle brackets stay literal characters
  4. the setter replaces every child
  5. untrusted string becomes real nodes

basics

~20 s

textContent treats the assigned string as literal text and shows it exactly as written; innerHTML runs it through the HTML parser and builds real nodes from it. Assigning untrusted data to innerHTML turns that data into page structure.

solid answer

~40 s

Both replace the element's children, but they interpret the string differently. `textContent` treats it as literal text: the browser creates one text node, so `<b>hi</b>` appears as those exact characters. `innerHTML` runs the string through the HTML parser, so the same input becomes a real `<b>` element with a text node inside it. Reading follows the same split — `textContent` concatenates the text of the element and all its descendants, while `innerHTML` serializes the children back to markup. The practical rule is that anything coming from a user, an API, or a database is a *value*, not markup, so it belongs in `textContent`. Putting it in `innerHTML` is how injected markup gets into a page: an inserted `<script>` element will not execute, but an `onerror` attribute on an injected `<img>` absolutely will.

code

javascript · 10 lines
javascript
const el = document.createElement('div');
const userInput = '<b>Ada</b> & Co';

el.textContent = userInput;
console.log(el.childNodes.length); // 1 (one text node)
console.log(el.innerHTML);         // "&lt;b&gt;Ada&lt;/b&gt; &amp; Co"

el.innerHTML = userInput;
console.log(el.childNodes.length); // 2 (<b> element + text node)
console.log(el.textContent);       // "Ada & Co"

go deeper

for a junior

Know that textContent shows the string exactly as written while innerHTML parses it into tags, and say plainly that user-supplied text goes into textContent.

for a middle

Explain the mechanics: both setters wipe the existing children, textContent inserts one text node, innerHTML runs the fragment parser. Be able to describe what each getter returns for the same subtree.

for a senior

Show the security judgment — name innerHTML as a markup sink, explain why blocking script tags is not a defence, and describe how you keep a controlled template while writing every variable value through textContent.

for a principal

Own the policy: how the codebase prevents markup sinks from being reachable by data at all, where the one sanctioned place for authored markup lives, and how that boundary is reviewed rather than remembered.

## The one-sentence difference `textContent` is about *text*. `innerHTML` is about *markup*. Every other consequence follows from that: whether angle brackets are characters or tags, whether the HTML parser runs, and whether a string supplied by someone else can add structure to your page. ## What the setters actually do Assigning to `textContent` discards all existing children and inserts exactly one text node holding the string verbatim. No parsing happens. `<`, `>` and `&` are just characters: ```js const el = document.createElement('div'); el.textContent = '<b>Ada</b> & Co'; el.childNodes.length; // 1 — a single text node el.innerHTML; // "&lt;b&gt;Ada&lt;/b&gt; &amp; Co" ``` Notice the second line: when you read the markup back, the browser *escapes* those characters, because that is the markup that would round-trip to the same text. Assigning to `innerHTML` also discards all existing children, but it first feeds the string to the HTML fragment parser and inserts whatever nodes come out: ```js el.innerHTML = '<b>Ada</b> & Co'; el.childNodes.length; // 2 — a <b> element and a text node el.textContent; // "Ada & Co" ``` The parser is forgiving. It closes unclosed tags, drops content that is illegal in the current context, and normalises what it can, so what you get back out is not always character-for-character what you put in. ## What the getters return Reading `textContent` walks the element and every descendant and concatenates their text — including text inside `<script>` and `<style>` elements and inside elements hidden with CSS, because no layout is consulted. Reading `innerHTML` serializes the children into a markup string, escaping text nodes as it goes. So the two getters answer different questions: "what characters are in here" versus "what would this subtree look like written as HTML". ## Where the difference becomes a bug The classic bug is a display bug: a username, a comment, or a product title contains `<`, `&`, or a stray tag, and it was assigned with `innerHTML`. Now `5 < 10` swallows the rest of the line, or a user called `<b>bob` makes the rest of the page bold. The serious version is injection. `innerHTML` is a markup sink, so a string that reaches it is a string that can create elements and attributes. A widespread half-truth is that this is safe because inserted `<script>` elements do not run — that part is true, the HTML spec marks scripts inserted via `innerHTML` as already-started so they never execute. But nothing stops an event-handler attribute: ```js el.innerHTML = '<img src=x onerror="console.log(\'ran\')">'; ``` The image fails to load and the handler fires. The same trick works with `onload`, `onfocus` plus `autofocus`, `<iframe srcdoc>`, and `javascript:` URLs. The rule is not "filter out script tags", it is "do not put untrusted strings into a markup sink at all". ## When you legitimately need both A common shape is a fixed template you control plus variable text you do not. Build the structure once with `innerHTML` (or write it in the HTML file), then fill the variable parts through `textContent` on the specific nodes: ```js card.innerHTML = '<h3 class="title"></h3><p class="body"></p>'; card.querySelector('.title').textContent = post.title; // safe card.querySelector('.body').textContent = post.body; // safe ``` If you need to append text next to existing content without touching it, `insertAdjacentText(position, text)` does the text-only equivalent of `insertAdjacentHTML`. ## innerText, the third option Browsers also expose `innerText`, which approximates the text *as rendered*: it respects CSS, so it skips elements hidden with `display: none`, collapses whitespace the way the layout does, and inserts line breaks for block boundaries. Because it consults layout, reading it forces the browser to bring style and layout up to date, which makes it meaningfully more expensive than `textContent`. Reach for `innerText` only when you specifically want what the user visually sees; otherwise `textContent` is the predictable, layout-independent choice. ## Choosing, in one line each Use `textContent` for any value. Use `innerHTML` only for markup you authored yourself. If a string could conceivably have come from outside your own source code, it is a value.

  • If an inserted <script> element never executes, why is innerHTML still treated as dangerous?
    Because scripts are not the only way to run code. The parser happily creates elements with event-handler attributes, so `<img src=x onerror=...>`, `<svg onload=...>`, `<iframe srcdoc=...>` and `javascript:` URLs all execute. Blocking the literal string `<script>` blocks one payload out of many; the fix is to keep untrusted strings out of markup sinks entirely.
  • How do you render a template you control while keeping user text safe inside it?
    Set the fixed structure once, then write the variable parts through `textContent` on the specific nodes: `card.innerHTML = '<h3 class="t"></h3>'` followed by `card.querySelector('.t').textContent = title`. The user string never reaches a markup sink. `insertAdjacentText` covers the case where you need to add text beside existing children.
  • What does reading textContent return for an element that contains a <script> or a hidden child?
    All of it. `textContent` walks the subtree and concatenates every text node, including the source inside `<script>` and `<style>` and the text of children hidden with CSS, because it never consults layout. `innerText` is the one that reflects rendering, skipping hidden content and collapsing whitespace — at the cost of forcing a layout update.

saying these in an interview costs you the question

  • innerHTML is just a faster way to set text
  • The browser escapes strings for you when you assign innerHTML
  • innerHTML is safe because injected script tags never run
  • textContent renders <b>bold</b> as actual bold text
  • Reading textContent gives you the element's HTML

context