Changing DOM Content

Learn how to read and update the text and HTML content of an element using textContent, innerHTML, and innerText, and understand the security risks of unsafe HTML injection.

Once you've selected an element, the next most common thing to do is change what's displayed inside it. JavaScript offers a few different properties for this, each with different behavior — and one of them (innerHTML) carries a real security risk if used carelessly with untrusted data.

textContent

element.textContent gets or sets the plain text inside an element, including text inside any nested elements, but ignoring all HTML tags entirely — both when reading (tags are stripped) and when writing (any string you assign is inserted as literal text, never parsed as HTML). This makes it the safest and fastest option when you're only working with text.

Because assigning to textContent never interprets its input as markup, <script> tags or other HTML you assign to it will show up on screen as visible text (like <script>alert(1)</script>) rather than being executed — this is exactly why it's the safe default for inserting user-supplied text.

innerHTML

element.innerHTML gets or sets the HTML markup inside an element — reading it returns a string of HTML tags and text, and writing to it parses the assigned string as real HTML and builds new DOM nodes from it. This is powerful: you can insert whole chunks of structured markup (like a card with a heading, image, and button) in one assignment.

That power is also the danger: if the string you assign comes from a source you don't fully control (user input, an API response, a URL parameter), innerHTML will happily create and execute anything it's given, including malicious <script> tags or event-handler attributes.

innerText

element.innerText is similar to textContent in that it deals with plain text, but it's aware of CSS styling and rendering: it only returns text that is actually visible on screen (excluding text inside elements hidden with display: none, for example), and it respects line breaks caused by block-level elements.

Because innerText depends on the rendered layout, reading it can be slower (it may trigger a layout recalculation) and its exact output can vary slightly depending on applied CSS — for most use cases, textContent is preferred unless you specifically need 'what a human sees on screen' behavior.

Safe HTML handling

The core safety rule is simple: use textContent whenever you are inserting text that a user could have influenced, and reserve innerHTML for markup you trust completely — content you wrote yourself, or content that has been properly sanitized by a trusted library specifically designed to strip dangerous HTML.

If you do need to build structured content dynamically from untrusted data, a safer pattern is to create individual elements with document.createElement() and set their text with textContent, rather than concatenating strings of HTML and assigning them via innerHTML.

XSS basics

XSS (Cross-Site Scripting) is a security vulnerability where an attacker gets their own malicious JavaScript to run inside your page, in the context of one of your users' sessions — often by tricking your code into inserting attacker-controlled text as HTML via innerHTML (or similar mechanisms) without sanitizing it first.

A classic example: a comment section that takes user input and inserts it directly via element.innerHTML = userComment lets an attacker submit a comment containing <img src=x onerror="stealCookies()">, which will actually execute for every visitor who views that comment. Using textContent instead of innerHTML for untrusted text closes this exact hole, because the browser never treats it as markup to execute.

Reading and writing content

textContent is the fast, safe default for plain text — always prefer it unless you specifically need to insert HTML markup. innerHTML parses its assigned string as HTML — never assign untrusted/user-supplied strings to it directly. innerText is layout-aware and can be slower; use it only when you specifically need visible-text-only behavior.

const box = document.querySelector("#box");

box.textContent;                 // plain text, tags stripped
box.textContent = "Hello!";      // sets plain text safely

box.innerHTML;                   // markup as an HTML string
box.innerHTML = "<strong>Hi</strong>"; // parsed and rendered as real HTML

box.innerText;                   // visible rendered text only

Code Examples

Sets plain text content on a status message element — the safe, common case for displaying any dynamic text to the user. Expected result: The element with id "status" now visually displays: Saved successfully!

status.textContent = "Saved successfully!" replaces whatever was inside #status with this exact text, no HTML parsing involved.

Uses innerHTML to insert a block of trusted, hardcoded HTML (written by the developer, not sourced from user input), building structured markup in one assignment. Expected result: #profile-card now contains a rendered <h2>Grace Hopper</h2> and <p>Computer scientist...</p>.

card.innerHTML = ... parses the template literal as real HTML, creating an <h2> and a <p> as actual child elements.

Common mistake: This is only safe because the string is a fixed literal written by the developer — if "Grace Hopper" and the bio came from user input instead, this exact pattern would be an XSS vulnerability.

Compares the unsafe innerHTML approach (commented out) with a safe approach that builds real DOM nodes and assigns user-supplied text only via textContent, so a malicious payload is displayed as harmless literal text instead of being executed. Expected result: The comment area displays: attacker: <img src=x onerror=alert('hacked')> as literal visible text — the payload does NOT execute. This is the essential defense against stored XSS in any feature that echoes user-supplied text back onto the page, such as comments, usernames, or chat messages.

strong.textContent = username assigns the username as plain text, so even a malicious string containing HTML tags is never interpreted as markup.

const status = document.querySelector("#status");
status.textContent = "Saved successfully!";
const card = document.querySelector("#profile-card");
card.innerHTML = `
  <h2>Grace Hopper</h2>
  <p>Computer scientist and Navy rear admiral.</p>
`;
function showComment(username, commentText) {
  const el = document.querySelector("#comment");

  // UNSAFE — do not do this with untrusted input:
  // el.innerHTML = `<strong>${username}</strong>: ${commentText}`;

  // SAFE — build elements and set their text directly:
  const strong = document.createElement("strong");
  strong.textContent = username;
  el.textContent = "";
  el.appendChild(strong);
  el.appendChild(document.createTextNode(": " + commentText));
}

showComment("attacker", "<img src=x onerror=alert('hacked')>");

Important Rules

Use textContent (or innerText) for plain text — it is fast and never interprets input as HTML.

Use innerHTML only for markup you trust completely; never assign raw user-supplied or unsanitized text to it.

innerText is layout-aware (visible text only) and can trigger layout recalculation; textContent is layout-independent and faster.

When building dynamic structured content from untrusted data, prefer document.createElement() + textContent over string-concatenated innerHTML.

Where this shows up

Every dynamic UI update touches this: showing a live character counter, rendering search results, displaying a user's name in a header, or building a comment feed. Security teams treat any innerHTML assignment fed by user data as a red flag in code review, precisely because XSS from careless innerHTML use is one of the most common real-world web vulnerabilities.

el.innerHTML = userSuppliedComment;

el.textContent = userSuppliedComment;

Assigning unsanitized user input directly to innerHTML lets an attacker inject and execute arbitrary HTML/JavaScript in your page (a Cross-Site Scripting vulnerability). textContent inserts the same string as inert, literal text instead.

console.log(hiddenElement.innerText); // may be empty or unreliable console.log(hiddenElement.textContent); // always returns the underlying text innerText only returns text that is actually rendered/visible on screen, so reading it from an element hidden by CSS (display: none) can return an empty string, surprising developers who expected the text content regardless of visibility. textContent ignores rendering entirely.

[Multiple Choice] Which property should you use to safely display user-supplied text without risking it being executed as HTML? A. innerHTML B. textContent C. outerHTML D. innerText, always, in every case

B. textContent — textContent always treats its assigned value as literal text, never as HTML to be parsed and executed, making it the safe default for untrusted or user-supplied strings.

[True / False] Assigning a string containing a <script> tag to element.innerHTML will always execute that script immediately. A. True B. False

B. False — False (with nuance). Browsers specifically prevent <script> tags inserted via innerHTML from executing automatically. However, innerHTML is still dangerous because other constructs — like <img src=x onerror=...> or event-handler attributes — DO execute, so treating innerHTML as safe for untrusted input is still a serious mistake.

Write a function renderWelcome(username) that safely displays 'Welcome, <username>!' inside an element with id "welcome", ensuring that even if username contains HTML like <b>hi</b>, it is displayed as literal text rather than being rendered as markup.

function renderWelcome(username) { const el = document.querySelector("#welcome"); el.textContent = Welcome, ${username}!; } renderWelcome("<b>hacker</b>"); // displays literally as: Welcome, <b>hacker</b>!

Use textContent, not innerHTML, so any HTML-looking characters in username stay as plain visible text.