innerText vs textContent in JavaScript Explained
Understand the differences between innerText and textContent in JavaScript. Learn how they handle whitespace, hidden elements, and performance so you pick the right one every time.
Both innerText and textContent look like they do the same thing: get or set text on a DOM element. But they handle whitespace, hidden elements, and performance differently. Mixing them up can break code formatting, leak hidden content, or create performance bottlenecks. This article explains every difference with examples so you always pick the right property.
Quick Overview
Before diving into details, here is the core distinction:
textContentreturns the raw text of every child node, including text inside hidden elements. It does not trigger a browser layout calculation.innerTextreturns only the visible (rendered) text. It reflects what the user actually sees, respecting CSS likedisplay: noneandvisibility: hidden. It triggers a layout reflow.
// HTML:
// <div id="example">
// Hello
// <span style="display: none;">Secret</span>
// World
// </div>
const el = document.getElementById("example");
console.log(el.textContent); // "Hello Secret World"
console.log(el.innerText); // "Hello World"Difference 1: Hidden Elements
The most important difference is how each property handles elements hidden by CSS.
// HTML:
// <ul id="nav">
// <li>Home</li>
// <li>About</li>
// <li style="display: none;">Admin Panel</li>
// <li>Contact</li>
// </ul>
const nav = document.getElementById("nav");
// textContent includes ALL text, even hidden items
console.log(nav.textContent);
// "Home About Admin Panel Contact"
// innerText only includes VISIBLE text
console.log(nav.innerText);
// "Home\nAbout\nContact"This distinction matters when building features like copy-to-clipboard or screen reader content. If you use innerText, hidden administrative links or debug information get excluded. If you use textContent, you get everything.
// Practical example: copy visible text to clipboard
function copyVisibleText(element) {
// innerText gives us only what the user sees
const visibleText = element.innerText;
navigator.clipboard.writeText(visibleText);
}
// Practical example: extract all text for indexing/search
function extractAllText(element) {
// textContent gives us everything
return element.textContent.toLowerCase().trim();
}Difference 2: Whitespace Handling
The textContent property preserves whitespace exactly as it appears in the HTML source. The innerText property collapses whitespace to match how the browser renders the text on screen.
// HTML:
// <p id="spaced">
// This has extra
// spaces and newlines
// </p>
const el = document.getElementById("spaced");
console.log(el.textContent);
// "\n This has extra\n spaces and newlines\n"
console.log(el.innerText);
// "This has extra spaces and newlines"This is especially important for <pre> elements and code blocks:
// HTML:
// <pre id="code">function hello() {
// return "world";
// }</pre>
const code = document.getElementById("code");
// textContent preserves the exact formatting
console.log(code.textContent);
// "function hello() {\n return \"world\";\n}"
// innerText may alter whitespace (browser-dependent)
console.log(code.innerText);
// "function hello() {\n return \"world\";\n}"| Property | Leading/trailing whitespace | Multiple spaces | Line breaks | Tab characters |
|---|---|---|---|---|
textContent | Preserved | Preserved | Preserved | Preserved |
innerText | Trimmed | Collapsed to single space | Matching rendered layout | Collapsed |
Difference 3: HTML Element Awareness
The innerText property understands block-level elements and inserts line breaks between them. The textContent property ignores element boundaries and concatenates all text nodes.
// HTML:
// <div id="content">
// <h2>Title</h2>
// <p>First paragraph.</p>
// <p>Second paragraph.</p>
// </div>
const content = document.getElementById("content");
// textContent: no separators between elements
console.log(content.textContent);
// "TitleFirst paragraph.Second paragraph."
// innerText: respects block boundaries with newlines
console.log(content.innerText);
// "Title\nFirst paragraph.\nSecond paragraph."The innerText property also converts <br> elements to newline characters:
// HTML:
// <p id="address">123 Main St<br>City, State 12345<br>Country</p>
const address = document.getElementById("address");
console.log(address.textContent);
// "123 Main StCity, State 12345Country"
console.log(address.innerText);
// "123 Main St\nCity, State 12345\nCountry"Difference 4: Performance
This is where textContent has a clear advantage. Reading innerText forces the browser to calculate the current layout (a "reflow") to determine what is visible. Reading textContent simply walks the DOM tree and collects text nodes.
const element = document.getElementById("large-content");
// Benchmark: read text 10,000 times
console.time("textContent reads");
for (let i = 0; i < 10000; i++) {
const text = element.textContent;
}
console.timeEnd("textContent reads"); // ~5ms
console.time("innerText reads");
for (let i = 0; i < 10000; i++) {
const text = element.innerText;
}
console.timeEnd("innerText reads"); // ~250ms (50x slower!)| Operation | textContent | innerText |
|---|---|---|
| Read (simple element) | ~0.001ms | ~0.02ms |
| Read (complex DOM, 1000 children) | ~0.1ms | ~5ms |
| Write (set text) | ~0.001ms | ~0.02ms |
| Triggers reflow on read | No | Yes |
| Triggers reflow on write | Minimal | Yes |
For write operations, the performance gap is smaller. But for read-heavy operations like search filtering or text indexing, textContent is significantly faster.
Difference 5: Setting Text
When setting text, both properties replace all child nodes with a single text node. The behavior is similar, with one key distinction: innerText converts newline characters (\n) to <br> elements.
const element = document.getElementById("output");
// textContent: newlines are NOT rendered as line breaks
element.textContent = "Line 1\nLine 2\nLine 3";
// Renders as: "Line 1 Line 2 Line 3" (single line in HTML)
// innerText: newlines ARE converted to <br> elements
element.innerText = "Line 1\nLine 2\nLine 3";
// Renders as three separate lines with <br> tagsBoth properties escape HTML, making them safe from XSS:
const userInput = '<script>alert("xss")</script>';
// Both are safe: HTML is escaped, not executed
element.textContent = userInput; // Shows the literal text
element.innerText = userInput; // Also shows the literal textWhen to Use Each Property
Use textContent When
// 1. Setting plain text (most common use case)
document.getElementById("count").textContent = `${items.length} items`;
// 2. Reading text for processing (search, filtering)
const allText = element.textContent.toLowerCase();
const matches = allText.includes(searchTerm);
// 3. Reading code or pre-formatted content
const codeText = document.querySelector("pre code").textContent;
// 4. Performance-critical loops
items.forEach(item => {
const text = item.textContent; // Fast, no reflow
// ... process text
});
// 5. Accessibility: getting all text including hidden content
const fullText = article.textContent;Use innerText When
// 1. Getting text that matches the visual rendering
const displayedPrice = document.querySelector(".price").innerText;
// 2. Copying visible text to clipboard
async function copyToClipboard(element) {
await navigator.clipboard.writeText(element.innerText);
}
// 3. Setting multi-line text with automatic <br> tags
const address = document.getElementById("address-display");
address.innerText = "123 Main Street\nApt 4B\nNew York, NY 10001";
// 4. Getting text with proper block-level line breaks
const articleText = document.querySelector("article").innerText;
// "Title\nFirst paragraph.\nSecond paragraph."Common Mistakes to Avoid
Mistake 1: Using innerText in Performance-Critical Code
// WRONG: innerText triggers reflow on every iteration
function filterList(searchTerm) {
const items = document.querySelectorAll(".list-item");
items.forEach(item => {
const text = item.innerText.toLowerCase(); // Reflow each time!
item.style.display = text.includes(searchTerm) ? "" : "none";
});
}
// CORRECT: textContent avoids reflow
function filterList(searchTerm) {
const items = document.querySelectorAll(".list-item");
items.forEach(item => {
const text = item.textContent.toLowerCase(); // No reflow
item.style.display = text.includes(searchTerm) ? "" : "none";
});
}Mistake 2: Using textContent for Multi-Line Display
// WRONG: textContent ignores newlines in the browser
const el = document.getElementById("output");
el.textContent = "Error on line 5\nMissing semicolon\nExpected }";
// Renders as single line: "Error on line 5 Missing semicolon Expected }"
// CORRECT: innerText converts \n to <br>
el.innerText = "Error on line 5\nMissing semicolon\nExpected }";
// Renders as three separate lines
// ALSO CORRECT: Use DOM methods
const lines = ["Error on line 5", "Missing semicolon", "Expected }"];
el.innerHTML = ""; // Clear
lines.forEach((line, i) => {
if (i > 0) el.appendChild(document.createElement("br"));
el.appendChild(document.createTextNode(line));
});Mistake 3: Using textContent to Read User-Facing Text
// HTML:
// <div class="product">
// <span class="name">Widget Pro</span>
// <span class="price">$29.99</span>
// <span class="stock" style="display: none;">OUT OF STOCK</span>
// </div>
const product = document.querySelector(".product");
// WRONG: textContent leaks hidden stock status
console.log(product.textContent);
// "Widget Pro$29.99OUT OF STOCK"
// CORRECT: innerText shows what the user sees
console.log(product.innerText);
// "Widget Pro\n$29.99"Real-World Example: Live Markdown Preview
This example uses both properties appropriately:
function createMarkdownPreview(editorId, previewId) {
const editor = document.getElementById(editorId);
const preview = document.getElementById(previewId);
const wordCount = document.getElementById("word-count");
editor.addEventListener("input", () => {
// Use textContent to read the raw input (preserves whitespace)
const raw = editor.textContent;
// Count words using textContent (fast, no reflow)
const words = raw.trim().split(/\s+/).filter(Boolean).length;
wordCount.textContent = `${words} words`; // Plain text update
// Simple markdown-to-html conversion
const html = raw
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
.replace(/^# (.+)$/gm, "<h1>$1</h1>")
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.+?)\*/g, "<em>$1</em>")
.replace(/`(.+?)`/g, "<code>$1</code>")
.replace(/\n/g, "<br>");
// Use innerHTML for the preview (controlled HTML, not user input)
preview.innerHTML = html;
});
// Use innerText to get a clean copy of the rendered preview
document.getElementById("copy-btn").addEventListener("click", () => {
const visibleText = preview.innerText;
navigator.clipboard.writeText(visibleText);
});
}
createMarkdownPreview("editor", "preview");Rune AI
Key Insights
- Hidden content:
textContentincludes hidden elements;innerTextexcludes them based on CSS visibility - Whitespace:
textContentpreserves raw whitespace;innerTextcollapses it to match browser rendering - Performance:
textContentis roughly 50x faster for reads becauseinnerTexttriggers a layout reflow - Line breaks: Setting
innerTextconverts\nto<br>elements;textContentdoes not render newlines - Default choice: Use
textContentas your default for getting and setting plain text; switch toinnerTextonly when you need rendering-aware output
Frequently Asked Questions
Is textContent or innerText better for accessibility?
Does innerText work in all browsers?
Can I use textContent on form elements?
Which property triggers fewer DOM reflows?
Should I ever use innerHTML instead of these properties?
Conclusion
The textContent and innerText properties serve different purposes despite their surface similarity. The textContent property gives you raw speed and access to all text including hidden content, making it the right default for most DOM text operations. The innerText property gives you rendering-aware text that matches what users see on screen, making it ideal for copying visible content or displaying multi-line text. Use textContent for setting plain text, processing text data, and performance-sensitive code. Use innerText when the visual rendering of the text matters.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.