Using innerHTML Safely in JavaScript DOM Methods
innerHTML lets you insert HTML into a page, but it carries a security risk. Learn how it works, the XSS danger, and safer alternatives for dynamic content.
innerHTML safety in JavaScript comes down to one rule: the property lets you read or replace the HTML inside an element using a string, and it is powerful, readable, and convenient. It is also the most dangerous DOM property when used carelessly.
Understanding both its power and its risk is essential before you use it.
const container = document.querySelector("#content");
container.innerHTML = "<h2>Welcome</h2><p>This paragraph was inserted.</p>";The container now contains a heading and a paragraph, parsed from the string and rendered as live DOM elements. In one line, you inserted structured HTML. This is why innerHTML is popular: it is concise and does exactly what you expect.
The Security Risk: Cross-Site Scripting (XSS)
The danger comes from where the HTML string originates. If any part of that string comes from user input, an attacker can inject elements that run their own script the moment the browser parses them.
A literal script element inserted through innerHTML will not run, browsers block that one specific case. But plenty of other elements execute code through event handler attributes, and innerHTML parses those just as readily:
const userName = '<img src="x" onerror="stealCookies()">';
const greeting = document.querySelector("#greeting");
// Dangerous: the broken image triggers onerror and runs the attacker's code
greeting.innerHTML = "Hello, " + userName;If userName comes from a URL parameter, a form field, or any user-controlled source, the browser tries to load the broken image, fails immediately, and fires the onerror handler. The attacker's code runs with the full privileges of your page.
The Safe Alternatives
For plain text, use textContent instead. It treats everything as text, so HTML tags in the input are displayed literally rather than parsed into real elements:
const userName = '<img src="x" onerror="stealCookies()">';
const greeting = document.querySelector("#greeting");
// Safe: displays the tag as literal text, no image element is ever created
greeting.textContent = "Hello, " + userName;The page now shows the angle brackets and attributes as plain text, not as a real img element. Nothing loads and nothing executes because textContent never parses HTML tags.
For building new elements programmatically, use createElement and appendChild. This approach never touches HTML strings, so there is nothing to inject. Start by clearing the container and creating each element separately:
const greeting = document.querySelector("#greeting");
greeting.textContent = "";
const heading = document.createElement("h2");
heading.textContent = "Welcome";
const paragraph = document.createElement("p");
paragraph.textContent = "This was built with createElement.";Neither createElement call ever touches an HTML string, and textContent sets each element's text safely. Now attach both new elements to the page:
greeting.appendChild(heading);
greeting.appendChild(paragraph);This pattern is completely immune to HTML injection because no string is ever parsed as markup. Every element and every piece of text is built and attached through direct DOM method calls instead.
When innerHTML Is the Right Choice
innerHTML is safe when the HTML string comes entirely from your own code with no user input mixed in. Common safe uses include:
- Setting up initial page structure from JavaScript
- Rendering static template content
- Inserting server-generated HTML that you trust
- Quickly clearing an element by setting innerHTML to an empty string
// Safe: the string is developer-written with no user input
const template = `
<div class="card">
<h3>Static Title</h3>
<p>This content is hardcoded.</p>
</div>
`;
document.querySelector("#app").innerHTML = template;The key rule is simple: if any part of the string came from a user, a URL, an API response you cannot fully trust, or any external source, do not pass it to innerHTML. When you do control the full string, innerHTML is often the fastest way to render a batch of markup in one step, rather than building each element by hand.
What Happens to Existing Content
Setting innerHTML replaces everything inside the element, not just the visible text. This includes existing text nodes, child elements, and any event listeners that were attached to those children:
const container = document.querySelector("#content");
container.innerHTML = "<p>New content</p>";All previous content and event listeners inside the container are gone. The browser destroys the old DOM subtree and builds a new one from the HTML string. If you need to preserve existing content or event listeners, use insertAdjacentHTML or build elements with createElement instead.
Where to Go Next
Now that you understand the safety rules, learn how to change text content safely with textContent or how to create DOM elements programmatically with createElement. For adding new HTML alongside existing content, see appending elements to the DOM.
Rune AI
Key Insights
- innerHTML parses HTML strings and inserts them as live DOM elements.
- Never pass raw user input to innerHTML; it enables XSS attacks.
- Use textContent for plain text, createElement for building new elements.
- innerHTML is safe for static, developer-written HTML strings.
- Overwriting innerHTML removes existing content and event listeners.
Frequently Asked Questions
Why is innerHTML dangerous?
When is it safe to use innerHTML?
What should I use instead of innerHTML for inserting text?
Conclusion
innerHTML is the right tool when you need to insert trusted HTML markup into a page. It is fast, readable, and convenient. But it is also the most dangerous DOM method when used carelessly. Never pass user input to innerHTML without sanitization. For plain text, use textContent. For building elements, use createElement. Reserve innerHTML for static HTML strings you control.
More in this topic
Using Reflect and Proxy Together in JavaScript
Proxy traps intercept operations, but Reflect is what makes them behave correctly. Learn why Reflect belongs inside every trap and what breaks when you skip it.
Top JS Array Methods Interview Questions to Know
The array method questions that come up most often in JavaScript interviews, answered directly with short examples: map vs forEach, mutating vs non-mutating methods, reduce, and more.
JavaScript Reflect API: Advanced Architecture
The Reflect object exposes JavaScript's own internal operations as plain functions. Learn every Reflect method, what it returns, and why it exists as its own API.