Entities exist because HTML has to distinguish markup from text using characters that also appear in ordinary writing. Once a document can say < to open a tag, it needs another way to say a literal less-than sign. That is the whole idea. Everything complicated about entities comes from the second question: which characters need it, and where.
The Five That Matter
In the body of a document, exactly two characters can change how the parser reads what follows. A < opens a tag. An & opens a character reference. Everything else is text.
Inside an attribute value the list grows by the quote mark that delimits it, so " in a double-quoted attribute and ' in a single-quoted one. The greater-than sign is the fifth by convention rather than necessity: a bare > in text is harmless in HTML5, and the spec says so, but escaping it costs nothing and protects you from the one real case, an unquoted attribute value.
That is the complete list for HTML: & < > " and '. Every other entity you have seen, from to — to ♥, is a convenience for typing or transport, not a correctness requirement.
Escaping Is Per Context, Not Per Document
The same string needs different treatment depending on where it lands. This is the part that HTML escaping alone does not solve.
| Where the text lands | What it needs | What goes wrong otherwise |
|---|---|---|
| Element body | HTML escaping | A < opens a tag and the rest of the page shifts |
| Quoted attribute | HTML escaping, quotes included | A matching quote closes the attribute early |
| Unquoted attribute | Quote the attribute first | A space ends the value and the rest becomes new attributes |
| Inside a script block | JavaScript string escaping | Entities are not decoded there; a quote breaks the script |
| URL or query parameter | Percent-encoding | An ampersand starts a new parameter |
| CSS value | CSS escaping | A closing brace escapes the rule and reaches the page |
The Case Where Entities Do Nothing
A script block is parsed as raw text, not as HTML. Character references are not decoded inside it. Writing & in JavaScript gives you those five literal characters, not an ampersand, and escaping user data as HTML before dropping it into a script does not make it safe. It only makes it wrong.
There is a related trap that catches people who do escape correctly: the sequence </script> inside a JavaScript string ends the block, wherever it appears. A string containing the literal text "</script>" closes the element and everything after it is parsed as HTML. The fix is to break the sequence, usually by writing <\/script>, and it is why JSON embedded in a page has to be escaped for the HTML parser as well as being valid JSON.
The general rule underneath both: you escape for the parser that reads the text next, in the order it reads it. HTML first, then whatever the content becomes.
Four Things Worth Knowing
None of these are edge cases. They come up in ordinary work, usually as a bug someone spends an afternoon on.
Double encoding has a signature
Text that reads &lt; was escaped twice: an already-escaped string went through the escaper again. Its ampersand became & and the rest stayed. The visible symptom is entities on the page instead of characters, and the fix is one more round of decoding, not another escaper.
' is younger than you think
It comes from XML and became valid HTML only in HTML5. In HTML 4 it is not defined, so old parsers print it literally. That is why every serious templating engine emits ' for an apostrophe and not the readable name.
The semicolon is not always required
Browsers still decode a legacy set without it, so © 2026 renders a copyright sign. This is why ¬it; famously decodes as ¬it; and why anything parsing HTML has to handle the case, however much the spec discourages writing it.
Numeric references cover all of Unicode
HTML 4 named 252 characters. HTML5 names 2,231, but neither covers emoji or most scripts, so 😀 is the only way to write one as a reference. Numbers work for every character that exists; names only for the ones somebody wrote down.
If the page is UTF-8, most entities are optional
Entities for accented letters and symbols solved a transport problem from the days of ASCII-only pipelines and mislabelled charsets. A document served as UTF-8 can contain café, — and 😀 directly, and doing so is smaller, more readable, and easier to search in your own source. Escaping beyond the five is worth doing when the output has to survive a channel you do not control, and not much else.
Escape or decode any text
Named, decimal or hex, with a table of every character it touched.
The Entities That Hide
Some references stand for characters you cannot see, and they cause a specific kind of bug: text that behaves oddly with nothing visibly wrong.
is a no-break space, U+00A0. It looks exactly like a space and never breaks a line, so a heading full of them refuses to wrap and overflows on a narrow screen. It also does not match a plain space in a search or a comparison, which is why text pasted from a word processor sometimes fails a lookup that should obviously succeed.
­ is a soft hyphen, invisible until the word breaks, at which point a hyphen appears. Useful in long compound words, mystifying when it lands in the middle of a slug or an identifier.
‌ and ‍ control whether adjacent characters join, which matters in Arabic and Indic scripts and in emoji sequences: a zero-width joiner is what turns separate emoji into a family. Any tool that lists what an entity resolves to should name these rather than print them, since printing them shows nothing at all.
Escaping Is Not Sanitising
The two get used interchangeably and are not the same operation.
Escaping converts characters so a parser treats them as data. It is lossless and reversible: the original text is recoverable, and nothing is judged or removed. Sanitising parses untrusted markup and removes what is dangerous, keeping the rest as markup. It is lossy and irreversible by design.
The choice is about intent. If the user's input is text and should stay text, escape it. If it is meant to be markup, a comment with formatting or a rich text field, sanitise it with a real library and keep the tags you allow.
The expensive mistake is escaping at the wrong time: escaping input on the way into the database rather than on the way out to a page. Store what the user actually typed, and escape at the moment you render, for the context you are rendering into. Escaping on input leaves you with a database of &quot; and no way to know which layer added it.
Frequently Asked Questions
Related Tools
Keep Reading
Markdown Is Not One Language: CommonMark, GFM and the Rest
It shipped in 2004 as a Perl script with no specification, so every renderer guessed. What CommonMark settled, what GitHub added, and where output still splits.
URL Encoding: encodeURI vs encodeURIComponent, and the Plus Sign
The two JavaScript functions are not interchangeable, and picking the wrong one is the most common URL bug there is. Plus where %2520 comes from.
Base64 Is Not Encryption: What It Does and Why It Exists
Three bytes become four characters, which is where the 33 percent size increase and the trailing equals signs come from. And why it hides nothing.
JSON Errors Explained: Trailing Commas, NaN and Other Rejections
Why valid-looking JSON fails to parse, how to read a parser's error position, and the number precision bug that silently corrupts large IDs.