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

FormSyntaxExampleNotes
LiteralThe 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 &amp; Jerry &lt;script&gt; &copy; 2026 &#8364;10 &#x1F600;</p>

<!-- the five characters that must always be escaped in text content -->
<!-- &  ->  &amp;      <  ->  &lt;      >  ->  &gt;
     "  ->  &quot;     '  ->  &#39; -->

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) => ({
  "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
}[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 &amp; Jerry          <- correct
escaped twice     : Tom &amp;amp; Jerry      <- renders as "Tom &amp; Jerry"

decoded twice     : Tom & Jerry              <- fine
                    but &lt;b&gt; becomes <b>  <- now live markup
SymptomCauseFix
The page literally shows &amp;Escaped twiceEscape once, at the point of output only
User text renders as boldDecoded after escapingNever re-decode after sanitising
Ampersands turn into entities in a saved formBrowser sends raw text, app escapes on readStore raw, escape only for display
Emoji becomes &#x1F600;Escaped for HTML but stored in JSONPick 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.

Escape sequences across languages Character sets: ASCII, Latin-1 and Windows-1252

Last refreshed 2026-09-18.