HTML entities and escaping in markup
Named and numeric character references, which characters must be escaped where, and how double-escaping turns one mistake into visible garbage in production.
Three ways to write the same character
| Form | Syntax | Example | Notes |
|---|---|---|---|
| Literal | The character itself | & | Only safe in a UTF-8 document |
| Named entity | &name; | & | A fixed list; not all code points have one |
| Decimal numeric | &#nnn; | & | Any code point by decimal value |
| Hex numeric | &#xhh; | & | Preferred for anything non-Latin |
The trailing semicolon is required by the specification, but browsers recover from a missing one in many cases. That leniency is not a licence to omit it: XML parsers reject the document outright.
<p>Tom & Jerry <script> © 2026 €10 😀</p>
<!-- the five characters that must always be escaped in text content -->
<!-- & -> & < -> < > -> >
" -> " ' -> ' -->The context decides the rule
- Text content — escape
&and<; escaping>is good practice. - Attribute value in quotes — also escape the quote character that delimits the attribute.
- Unquoted attribute — a space ends the value, so never build one dynamically.
- URL in an attribute — escape the HTML, then URL-encode the query separately. The two encodings are independent.
- Script and style blocks — HTML entities are not recognised inside them, so a closing tag sequence in a string ends the block.
// the safe approach: set text, let the platform escape
el.textContent = userInput; // never innerHTML for untrusted text
el.setAttribute("data-note", userInput); // the DOM escapes the value for you
// if you must build a string, escape every context separately
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
}[c]));
el.innerHTML = "<b>" + esc(userInput) + "</b>";Serialising JSON into a script block needs a different rule: escape < as \u003c so no closing tag can appear in the data, and avoid the literal sequence that ends the block.
Double escaping and double decoding
user types : Tom & Jerry
escaped once : Tom & Jerry <- correct
escaped twice : Tom &amp; Jerry <- renders as "Tom & Jerry"
decoded twice : Tom & Jerry <- fine
but <b> becomes <b> <- now live markup| Symptom | Cause | Fix |
|---|---|---|
The page literally shows & | Escaped twice | Escape once, at the point of output only |
| User text renders as bold | Decoded after escaping | Never re-decode after sanitising |
| Ampersands turn into entities in a saved form | Browser sends raw text, app escapes on read | Store raw, escape only for display |
Emoji becomes 😀 | Escaped for HTML but stored in JSON | Pick one layer for each escaping step |
⚠️
Escaping is not sanitisation. Escaping makes text safe to display; sanitisation removes dangerous markup from trusted-HTML pipelines. Applying escaping to already-sanitised HTML breaks the markup, and skipping sanitisation on trusted HTML opens an injection hole.
FAQ
Should I use named or numeric entities?
Numeric hex is the most portable and covers every code point. Named entities are more readable in hand-written markup, but the list is finite and some names changed in HTML5.
Do I need to escape inside a script block?
No HTML entities there, but you must prevent the closing tag sequence from appearing in data. Escape angle brackets in the JSON, or load the data from a separate request instead.
Related
Escape sequences across languages Character sets: ASCII, Latin-1 and Windows-1252
Last refreshed 2026-09-18.