HTML Entity Decoder_

Turn <, ' and   back into the characters they stand for — and find out whether your string went through an encoder more than once.

That is the usual reason a decoder looks broken. Text reading < has been escaped twice: one pass leaves you holding <, which looks like nothing happened. This counts the passes and shows you each one.

toolkit.codes/html-decode
Depth
Paste something encoded
UTF-8
Ready
100% LOCAL
Input
Text containing named, decimal or hexadecimal character references, at any depth.
Output
The decoded text, the number of passes it took, and the string as it looked after each pass.
Processing
String work in this tab. No DOM is involved, so the result cannot depend on what a browser would have tolerated.
Limits
Decoding is not sanitising. Text that is safe as markup after one pass may not be after three, which is why the depth is a choice here rather than a default.
Unknown references survive
A reference this does not recognise is left exactly where it was and reported, because a string containing &foo; is more often a query parameter than a broken entity.

Double encoding, and why it looks like a bug in the decoder

One string, two escapes

A template escapes a value on the way into a page. A framework escapes it again on the way out, or a CMS escaped it before storing it, or an API returned it pre-escaped and the view layer did not know. The ampersand of < is itself a character that needs escaping, so it becomes <, and a reader sees the literal text < printed on the page where a less-than sign should be. Decoding once gives you < and the strong impression that the tool did nothing.

The depth is the diagnosis

This page decodes repeatedly until the text stops changing, and reports how many passes that took. One is ordinary. Two means something in your pipeline escaped the same value twice, and knowing that is more useful than the decoded string — because the string is a symptom and the double escape is the bug. Three or more usually means a value has been round-tripping through a store that escapes on write and a view that escapes on read, gaining a layer each time it is edited.

Decoding all the way is not always what you want

This is the part a decoder should say out loud. Escaping is what stops untrusted text being read as markup, and decoding is that protection removed. <script> is inert; it is inert precisely because something escaped it twice. Decode it fully, put the result back into a page, and it is a script tag again. So the full decode is offered as a deliberate choice rather than as the default, and if the text is untrusted and going back into a document, the correct number of passes is usually zero.

What is deliberately not touched

A reference this does not know — &foo; — comes through untouched, byte for byte, and gets named in the panel underneath. Deleting it would look tidier and would corrupt data, because a string of that shape is far more often a URL query parameter than a broken entity. A bare ampersand with no semicolon is left alone for the same reason. Numeric references pointing at surrogates or beyond the last code point are not characters, so they are reported rather than turned into replacement marks.

Paste, read the depth, then choose

  1. 01Paste the text. Decoding runs immediately at the depth currently selected.
  2. 02Read the line underneath before the output. It says how many passes the text needed, and one is the ordinary answer.
  3. 03If it says two or more, look at the ladder: the string is shown after each pass, which is what makes a double escape visible rather than theoretical.
  4. 04Choose the depth deliberately. Once is right for reading a value; all the way is right for diagnosing a pipeline; neither is right for untrusted text you are about to put back into a page.

Four things that bring people here

Entities printed on the page

A visitor sees <p> instead of a paragraph.

Depth reported
2
Means
something escaped it twice

An API response full of "

JSON whose string values arrived pre-escaped for HTML.

Depth
Once
Then
store the decoded value, escape at render

A database column gaining layers

Every edit adds another &.

Depth grows
3, then 4, then 5
Cause
escaping on write and on read

Going the other way

You need to make text safe rather than readable.

Use
the HTML encoder
Which
escapes the five that matter

What a decoder does with each shape

InputResultReasoning
&lt;<A named reference this knows
&#60;<Decimal, the form that works in every parser ever written
&#x3C;<Hexadecimal, case-insensitive in both the x and the digits
&amp;lt;&lt; then <Two passes. The depth readout is the point of this row
&foo;&foo;Unknown. Left alone and reported — far more often a query parameter than an entity
a & ba & bA bare ampersand is not a reference and is not touched
&#xD800;unchangedA surrogate is not a character on its own, so this is reported rather than decoded
&#999999999;unchangedPast the last code point. Reported for the same reason

Nothing in this table is deleted. A decoder that removes what it does not understand produces output that looks clean and quietly differs from the input.

Habits that stop the double escape coming back

  • Store the raw value and escape at render. Escaping on the way into storage is what produces columns that gain a layer every time somebody edits the row.
  • Escape exactly once, at the boundary where the text becomes markup. Every additional layer is a bug that has not been noticed yet.
  • If a template engine auto-escapes, do not escape before handing it the value. Nearly every double-encoding bug is a manual escape in front of an automatic one.
  • A depth of two in production data is worth a ticket, not a workaround. Decoding twice at render hides it and the layers keep accumulating underneath.
  • For text arriving from an API, decode once on receipt and store the result — not on display, where it will be escaped again by the template.
  • To go the other direction, use the HTML entity encoder. Its default escapes only the characters that change how a document parses, which is the correct set.

Where decoding is the wrong move

Decoding untrusted text puts the markup back

Escaping is the protection; decoding removes it. &lt;script&gt; is inert text and stays inert until something decodes it into a tag. If the value is untrusted and is going back into a document, decoding it is the vulnerability rather than the fix.

Decoding twice at render hides a real bug

A page showing entities can be patched by decoding harder in the view. The double escape upstream is still there, still adding a layer on every edit, and now nothing surfaces it. The depth readout here exists so the diagnosis is available before the workaround is.

A browser is a lenient decoder and a bad reference

Assigning to innerHTML decodes things this will not — unterminated references, obsolete names, malformed numerics — because HTML parsing is specified to recover from almost anything. That leniency is why decoding through the DOM gives different answers in different contexts. This is a string operation and does not vary.

Deleting unknown references corrupts data

Text containing &utm_source=x is a URL, not a broken entity. Tools that strip anything ampersand-shaped that they cannot resolve silently mangle query strings, and the damage is invisible until somebody follows the link.

Entities are not encryption or encoding of bytes

They describe characters for an HTML parser and nothing else. Percent-encoding for URLs and Base64 for binary are different problems with different rules, and running text through the wrong one produces something that looks decoded and is not.

What is decoded, and what is refused

Recognised
Named references, decimal &#60; and hexadecimal &#x3C; in either case
Depth
Decoded repeatedly until the text stops changing, capped at ten passes. The count is reported, and each intermediate string is shown when there is more than one
Unknown references
Left in place, byte for byte, and listed. Never deleted — that shape is more often a query parameter than an entity
Invalid numerics
Surrogates and values past the last code point are reported rather than decoded, because neither names a character
No DOM
String work only. Decoding through innerHTML inherits the HTML parser’s error recovery, so the same input can decode differently depending on where it lands. This does not vary
Not sanitisation
This makes text readable, not safe. Decoded untrusted text is exactly what escaping was preventing, and the interface makes the depth a choice for that reason
Scope
Decoding only. The escaping direction, the entity reference table and the double-encoding warning on input live on the HTML entity encoder
Network
None from tool code. A test sweep calls every function this page uses with fetch and XMLHttpRequest replaced by stubs that throw, so a stray request fails the build instead of shipping. Disconnect from the network and the page still works.

Questions about decoding HTML entities

Why does my text still show &amp;lt; after decoding?

Because it was encoded twice, and one pass only removes one layer. The original < became &lt;, and then that ampersand was escaped in turn to give &amp;lt;. Switch the depth to "all the way" and this reports how many passes it actually took — two is the common answer, and it points at the real bug.

What causes double-encoded HTML?

Two things escaping the same value without knowing about each other. A manual escape in front of a template engine that already auto-escapes is the commonest. Escaping before storing and again before rendering is the next, and it is worse because the layers accumulate: every edit of that row adds one.

Is it safe to decode HTML entities?

Safe to read, not safe to re-publish. Escaping is what stops text being parsed as markup, so decoding is that protection deliberately removed. If the text came from a user and is going back into a page, decoding it is how the escaping gets defeated — which is why the full decode here is a choice you make rather than what happens by default.

Do I need to care which form an apostrophe arrived in?

Not when decoding. Named, decimal and hexadecimal references for the same character all decode to the same character here, so &apos;, &#39; and &#x27; are interchangeable on the way in. Which one to write is a real question with a real answer, and it belongs to the encoder.

Why is &amp;foo; left in my output?

Because it is not an entity this knows, and guessing is worse than leaving it. Text of that shape is usually a URL query parameter — &utm_source=x is the everyday example — and a decoder that strips whatever it cannot resolve silently breaks links. Anything left alone is listed underneath so you can see it happened.

Should I decode with innerHTML instead?

It works and it is not a reference implementation. The HTML parser is specified to recover from malformed input, so it decodes unterminated and obsolete references that a strict decoder will not, and it behaves differently inside an attribute than in text. If you want a result that is the same everywhere, do it as string work — which is what this is.

Is the text I paste here sent anywhere?

The work is JavaScript running in this tab. Every function it calls is covered by a test that stubs fetch and XMLHttpRequest to throw, so a request that slipped in would break the build rather than reach a server — and you can confirm it for yourself by disconnecting and carrying on.