Why an HTML Entity Decoder Should Use the Browser's Parser, Not Regex
Turning & into & and < into < sounds like a job for a few lines of regex replace calls — but the HTML5 standard defines over 2,000 named entities, things like ©, ♥, and α. There's a way to decode all of these correctly without hardcoding every one of them: borrow the browser's own HTML parser and let it do the work. This guide explains how that trick works, and why it specifically uses <textarea> instead of <div>.
1. Why regex alone isn't enough
HTML entities fall into three broad categories: named entities like <, decimal numeric entities like ©, and hexadecimal numeric entities like ©. Numeric entities are straightforward for regex — you just compute the code point directly — but named entities can only be converted correctly if you know the full list defined by the WHATWG HTML5 standard (2,231 names, more still if you count case-sensitive variants). A decoder that hand-maps only a handful of entities like &, <, >, and " will either leave the remaining thousands of entities — © (©), ™ (™), € (€), α (α), and so on — untouched, or get them wrong.
2. The trick: borrow the browser's own HTML parser
The way around this problem, instead of building your own mapping table, is to hand the job to the HTML parser the browser has already implemented perfectly. Assign the string you want to decode to a DOM element's innerHTML, then read that element's text content back out — and you get a result with the browser's complete, standard entity table already applied. The browser already knows every named entity, numeric entity, and hexadecimal entity defined in the spec, so there's no separate mapping data to maintain.
3. Why <textarea> and not <div>
There's one important choice here. Applying the same trick to a <div> would decode entities just as well. The problem is safety. The HTML5 parsing spec defines a different "content model" for each element type: <div> follows the normal content model, so if the string assigned to its innerHTML contains an actual tag like <script> or <img onerror=...>, the browser parses it as a real child DOM element — and if that result is later rendered back onto the page, it opens an XSS path where the script could actually execute. A <textarea>, by contrast, is classified under the HTML5 spec as having the RAWTEXT content model. A RAWTEXT element treats everything inside it as pure text until it hits its closing tag, and never parses strings like <script> inside it into separate, live DOM elements. In other words, the entities get decoded correctly, but the safeguard against a tag-like string in the result turning into an executable element is built into the structure itself.
html-decoder.html uses exactly this approach. const ta = document.createElement('textarea'); ta.innerHTML = text; const decoded = ta.value; — it creates a temporary textarea, assigns the original text to its innerHTML, and reads the decoded plain text back out via .value. This temporary element is never attached to the DOM and never rendered on screen, so there are zero side effects.
4. What's often missed in practice: what happens after decoding matters more
One thing worth stressing here: "entity decoding itself" is completely safe with the approach above. The risk comes from what you do with the decoded result afterward. If you display the decoded text on screen using textContent, it's always safe — but if you insert that same result back into innerHTML, then any tags in it really can get parsed and executed this time. In short, remember that "the decoding function" being safe and "where you put the decoded value" are two entirely separate security decisions.
5. A few edge cases with numeric entities
The HTML5 parsing spec doesn't let every numeric reference pass through unchanged. For example, a reference to the null code point like �, or a value in the surrogate range (U+D800–U+DFFF), is treated as a parse error and replaced with the Unicode replacement character (U+FFFD, �). Edge cases like these are handled automatically per spec when you leave the job to the browser's parser — but if you write your own regex-based numeric entity decoder, you have to account for these exceptions yourself.
Frequently Asked Questions
Q. Why not just hardcode all the named entities?
A. You could, but then you'd have to maintain over 2,000 standard entities (more still counting case-sensitive variants), and revisit that list every time the HTML5 standard is updated. Borrowing the browser's parser removes this maintenance burden entirely.
Q. Is using textarea instead of div really free of execution risk?
A. Yes. Under the HTML5 spec, textarea has the RAWTEXT content model, so nothing inside content assigned to its innerHTML ever gets parsed into an actual child DOM node. Even if a script tag string ends up inside it, it never executes — it's simply treated as text.
Q. Is it okay to insert the decoded result back into innerHTML?
A. Not recommended. Decoding itself is safe, but if the resulting string happens to contain a tag, inserting it via innerHTML will parse it into a real DOM element that can execute. If your goal is just to display it as text on screen, always use textContent instead.
Q. How does this handle double-encoded strings?
A. This approach only strips one layer of encoding per pass. If the original text was encoded twice (for example, < encoded again into &lt;), you need to run the decoder twice in a row to fully recover the original text.