JS DOM: innerHTML, textContent, innerText, nodeValue

By Xah Lee. Date: . Last updated: .

innerHTML

innerHTML is meant to be HTML code.

// change content of first p tag
document.getElementsByTagName("p")[0].innerHTML = "<b>water</b>";
// the word water is shown as bold

textContent

// change content of first p tag

document.getElementsByTagName("p")[0].textContent = "<b>water</b>";

when you set document.getElementsByTagName("p")[0].textContent = "<b>water</b>"; the result is not bold. but the html b tag means it should be. How to explain this conflict?

answer

(ai answer)

There is no conflict. <b> only makes text bold when the browser parses that string as HTML. textContent never does that.

What textContent actually does

textContent is defined as the plain-text content of a node. Assigning to it:

  1. Removes all current child nodes.
  2. Creates a single text node whose data is the string you gave it, character for character.
  3. Does not run the HTML parser.

So this:

document.getElementsByTagName("p")[0].textContent = "<b>water</b>";

puts the literal characters <, b, >, w, a, t, e, r, <, /, b, > into the page. The browser displays them as text, not as a <b> element. That is the specified, intended behavior.

Contrast with innerHTML

innerHTML does feed the string to the HTML parser, which builds real element nodes:

document.getElementsByTagName("p")[0].innerHTML = "<b>water</b>";

Now the paragraph contains a <b> element whose text is “water”, so it renders bold. That is why the two properties exist: one is for text, one is for markup.

Safer / more explicit alternatives

If you only want the word “water” bold and you control the markup, create the element yourself:

const p = document.getElementsByTagName("p")[0];
p.textContent = "";          // clear
const b = document.createElement("b");
b.textContent = "water";     // still plain text
p.appendChild(b);

Using textContent (or createTextNode) for user-supplied strings is also the usual recommendation against XSS, because those APIs never interpret < as the start of a tag.

but still, it creates a inconsistency because now, apparantly, part of the html can be in a state of not parsed. does this happen in other circumstances?

Yes. That state is normal. The inconsistency only appears if you treat the live page as one HTML string that must stay fully parsed. After load, the document is a tree. Markup is parsed only at specific moments. Everything else can hold characters that look like tags without those characters ever becoming elements.

The document is a tree, not a source file

The HTML parser runs when:

  • the original document is loaded
  • you assign innerHTML / outerHTML
  • you call insertAdjacentHTML, DOMParser, document.write, etc.

Between those moments, a text node is just a string of Unicode. <b>water</b> inside a text node is the same kind of data as hello or 2 < 3. The browser does not scan existing text nodes looking for tags.

“View Source” shows the original bytes. DevTools Inspector shows the live tree. After your textContent assignment, Inspect shows a single text node whose data is the characters <b>water</b>. That is the real document.

This happens all the time

1. Any API that writes text, not markup

textContent, innerText, nodeValue, createTextNode(), document.createTextNode(), form values copied into the page, textContent of user comments, etc. All of these can contain <, >, &.

2. Elements the parser itself treats as raw / RCDATA text

While parsing source HTML, these do not interpret nested tags as elements (with some end-tag exceptions):

  • script, style — raw text
  • textarea, title — RCDATA (entities decode, tags do not)
  • historically xmp, iframe body in some modes, noscript when scripting is on, obsolete plaintext

So this is valid HTML and the <b> never becomes bold:

<textarea><b>water</b></textarea>
<title>Use <b> for bold</title>
<script>const s = "<b>water</b>";</script>

3. Comments and similar

<!-- <b>water</b> --> is a comment node. The string is not parsed as elements.

4. Attributes

data-note="<b>water</b>" stores that string on the attribute. It is not a <b> element. Serialization now typically escapes < and > in attributes so a later parse does not invent tags.

5. Foreign content, templates, inert trees

SVG/MathML have different rules. <template> holds an inert fragment until you clone it. JSON in <script type="application/json"> is text.

Why it does not “break HTML”

If you read innerHTML after the textContent assignment, the serializer escapes the text node so the string round-trips as text, not as a tag:

&lt;b&gt;water&lt;/b&gt;

Parse that again and you still get a text node, not a bold element. The tree and the serialized markup stay consistent; they just encode “this is text” differently from “this is an element.”

(There are special cases: serializing text inside script/style does not escape the same way, which is why mutation-XSS bugs exist around those elements.)

Mental model

Layer Role of <b>water</b>
HTML source / innerHTML setter Markup: parser may create a <b> element
Text node / textContent Data: those eight characters, including < and >
innerHTML getter Serialization: usually &lt;b&gt;water&lt;/b&gt; so a later parse keeps it as text

So the page is not “partly unparsed HTML.” It is a DOM in which some nodes are elements and some nodes are text. Text that resembles tags is an everyday case, not a special broken state. Parsing is an operation you invoke, not a permanent property of every substring in the tree.

textContent vs innerText

What is the difference between textContent and innerText

(ai answer)

textContent and innerText both get/set the text of a DOM node (ignoring HTML tags), but they differ significantly in what they include, how they handle styling/visibility, performance, and scope.

Key Differences

Aspect textContent (Node) innerText (HTMLElement)
What it returns All text content of the node and its descendants, including hidden text, <script>, <style>, comments ignored, raw whitespace/line breaks from the markup Approx. the rendered/visible text a user would see (or get by selecting + copying). Ignores hidden elements, normalizes whitespace, respects <br>, and applies some CSS text transformations
CSS / visibility Completely ignores CSS and layout. Includes display: none, visibility: hidden, etc. Aware of CSS styling and computed styles. Skips non-rendered/"human-readable" content
Performance Fast — no layout/reflow needed Slower — reading it can trigger a reflow to get up-to-date styles
Availability All Node types Only HTMLElements
Setting value Replaces all children with a single text node Same, but converts line breaks into <br> elements
Standardization Long-standing DOM standard Originally non-standard (IE); later standardized

Practical Example

<div id="demo">
  <style>p { color: red; }</style>
  Visible text.
  <span style="display: none">Hidden text</span>
  <br>
  More text.
</div>

When to Use Which

Note: Neither parses HTML (unlike innerHTML). Setting either one removes existing child nodes. Both are widely supported in modern browsers.

nodeValue

The most compatible and standard way of setting text content is first to get the element's content as a text node, then set nodeValue, like this:

node.firstChild.nodeValue = text

But, if a element's content is empty, it doesn't have any child (thus no text node), so you need to check if element has firstChild first.

const f_addTextNode = (xel, xtext) => {
 while (xel.firstChild) xel.removeChild(xel.firstChild);
 xel.appendChild(document.createTextNode(xtext));
};

f_addTextNode(document.getElementsByTagName("p")[0], "good");

History