Filling an area of the page with several elements sometimes fits on one line. That is the promise of innerHTML, and the reason for its popularity.
It is also the property causing the most silent damage, from the field emptied for no reason to the security hole opened without anyone noticing.
Definition
innerHTML stands for the HTML content of an element, as a string. Read from, it hands back the markup of the children. Written to, the browser parses the string given, builds nodes from it, and replaces all the existing content.
const list = document.querySelector("#cart");
list.innerHTML = "<li>Keyboard</li><li>Mouse</li>";
console.log(list.children.length); // 2
console.log(list.innerHTML);The difference with textContent is essential: the latter treats the string as plain text, so the tags then show up on screen exactly as written.
What writing destroys
The assignment does not edit the existing nodes, it throws them away and builds new ones. Everything attached to the old ones disappears with them.
- Listeners placed by addEventListener() on the children are lost.
- Form fields go back to their original value, including text already typed in.
- Focus and scroll position are reset.
Writing element.innerHTML += "..." makes it worse: it reads all the content back, concatenates, then rebuilds the whole thing. Over a loop of a hundred items, the work is redone a hundred times.
The injection risk
Inserting a string that came from a visitor amounts to letting that person write HTML into the page. This is the flaw known as script injection, and it allows a session token to be read or data to be sent elsewhere.
const message = '<img src="x" onerror="alert(1)">';
zone.innerHTML = message; // the code runs
zone.textContent = message; // the text shows up, harmlessA <script> tag inserted through innerHTML does not run, that much is true. It is no protection though: event attributes such as onerror or onload work perfectly well.
The alternatives
| Spelling | When to use it |
|---|---|
textContent | As soon as text is involved, so most of the time |
insertAdjacentHTML | Adding markup without rebuilding what is there |
createElement | Building elements whose reference you want to keep |
Frequently asked questions
Is it faster than creating elements one by one?
For one large insertion in a single pass, parsing a string is often competitive. The advantage flips as soon as the property is rewritten in a loop, and createElement() keeps a reference on each element, which saves searching for it afterwards.
How do you add without rebuilding everything?
insertAdjacentHTML takes a position and a string, and inserts without touching the neighboring nodes. The four accepted positions are beforebegin, afterbegin, beforeend and afterend, the last one placing the content right after the element.
How can HTML be inserted safely?
The rule fits in one sentence: only insert markup you wrote yourself, never a string coming from a form or an interface. Interface libraries settle the matter differently, React escaping text by default and demanding a deliberately off-putting property name to insert raw HTML, a mechanism covered in the React course.