Encode reserved characters, not every character
HTML reserves a small set of characters because they control markup. Everything else is just text:
| Character | Encode in… | Named entity |
|---|---|---|
< | content | < |
> | content | > |
& | content & attributes | & |
" | double-quoted attributes | " |
' | single-quoted attributes | ' / ' |
If a character isn’t structurally significant in the spot you’re putting it, it doesn’t need encoding. That’s the whole rule — and it’s why an entity encoder is a precision tool, not something to blanket-apply to your text.
The XSS truth: encoding is context-specific
The single most important thing to understand is that HTML entity encoding only protects the HTML context — element content and attribute values. The browser decodes entities before handing text to other parsers, so entity-encoding does nothing in these places:
- JavaScript context (
onclick, inline<script>) — needs JS string escaping; entities are decoded then executed. - URL context (
href,src) — needs percent-encoding, andjavascript:URLs must be blocked entirely. - CSS context (inline
style) — needs CSS escaping.
So “I HTML-encoded the input, am I safe from XSS?” — only if the input lands in an HTML context. Defense-in-depth means encoding for the specific context where the data is used, which is why frameworks ship context-aware escapers rather than one universal encode function.
Named, decimal, or hex — does it matter?
All three forms render identically — &, &, and & all produce &. Named entities are the most human-readable and are the right default for hand-written HTML. Numeric forms (decimal/hex) matter when a character has no named entity, or for maximum parser compatibility in older or non-HTML contexts (XML, for instance, only defines five named entities). Use named for readability; reach for numeric for the long tail of symbols.
The invisible-character gotcha
One decode result trips people constantly: decodes to a non-breaking space (U+00A0), which looks identical to a normal space but is a different character. It won’t match " " in comparisons and trim() won’t remove it — a frequent cause of “why does this string equality fail?” bugs. After decoding, if you need plain text, normalize U+00A0 to a regular space. The decoder here handles 2,000+ named entities, so you can paste encoded text and see exactly what it resolves to, invisible characters included.