HTML Entities Explained: When to Escape &, <, > and Quotes
An HTML entity is not decoration. It is the only way to put a character into a document when that character would otherwise be read as markup, and the set of characters that qualify changes depending on where in the document you are standing. Get it wrong in one direction and you get a broken page or an injected script; get it wrong in the other and your users read &amp; in their own name. This guide covers which characters matter, in which context, and how to escape them exactly once.
In short
Escape & and < everywhere in HTML, plus whichever quote character delimits an attribute value; > is escaped by convention rather than by requirement. Use &, <, >, " and ', and little else. Character references are decoded in element text, in attribute values and inside title and textarea, but never inside a script or style element, where they do nothing at all.
What an entity actually is
The HTML parser reads a document as a stream of characters and decides, character by character, what is markup and what is text. Two characters are special: < starts a tag, and & starts a character reference. Everything called an "entity" is really a character reference — an escape sequence that says "the author meant this character as data, not as markup" — and the parser replaces it with the character before anything else sees the document.
There are three spellings. A named reference such as & uses one of the names the HTML Standard defines; a decimal reference such as & uses the Unicode code point in base ten; a hexadecimal reference such as & uses the same number in base sixteen. All three produce exactly the same character, and all three are decoded in exactly the same places. Nothing else about them differs, so the choice is entirely about who has to read the source.
Where the parser decodes them is the part worth memorising, because it is not everywhere. References are decoded in ordinary element text, in attribute values, and inside the two "escapable raw text" elements, title and textarea. They are not decoded inside script or style, which the standard classifies as raw text elements — an & written there stays four characters forever. This one rule explains a large share of both broken output and successful injections.
| Character | Named | Decimal | Hex | Must escape in |
|---|---|---|---|---|
| & (ampersand) | & | & | & | Text and attribute values — always |
| < (less-than) | < | < | < | Text — required by the HTML syntax rules |
| > (greater-than) | > | > | > | Text — by convention, not by requirement |
| " (double quote) | " | " | " | Double-quoted attribute values |
| ' (apostrophe) | ' (HTML5/XML only) | ' | ' | Single-quoted attribute values |
| non-breaking space | |   |   | Never required — used deliberately |
The five characters, and where each one matters
The ampersand is the only character that must be escaped in every context, because it is the character that starts an escape. A literal & in text or in an attribute value is legal as long as it is not what the standard calls an ambiguous ampersand: "a U+0026 AMPERSAND character (&) that is followed by one or more ASCII alphanumerics, followed by a U+003B SEMICOLON character (;), where these characters do not match any of the names given in the named character references section". That definition is a trap in URLs, where &sort=new and ©=1 sit in query strings all day, so the reliable rule is to write & and stop thinking about it.
The less-than sign is next, and it is the one the specification genuinely forbids: normal element text "must not contain the character U+003C LESS-THAN SIGN (<) or an ambiguous ampersand". The greater-than sign is different — nothing in the HTML syntax rules requires it to be escaped in text, because a > with no matching < is simply data. Every escaping library escapes it anyway, and so should you: it costs three characters, it makes the output safe to paste into XML, and it removes an entire category of argument from code review.
The two quote characters are contextual. Inside a double-quoted attribute value, a raw " ends the value and everything after it becomes new attributes; inside a single-quoted one, a raw ' does the same. Since an escaping function usually cannot know which quoting style the template will use, escape both. For the apostrophe, prefer the numeric ' over ': the named form is defined in XML and in HTML5, but not in HTML 4, and unlike &, <, > and © the name &apos is not one of the legacy references that parse without a trailing semicolon.
Anything else is optional. Accented letters, currency symbols, dashes and arrows can all be written as themselves in a UTF-8 document, and a UTF-8 document is the only kind you should be serving. Named references such as é and € are a legacy of single-byte encodings; they still work, they still bloat the source, and they still confuse anyone trying to grep for a word. If a page needs an HTML formatter run over it to be readable, entity soup is usually part of the reason.
Named, decimal or hex: choosing a spelling
The WHATWG list currently defines 2,231 named character references, which is far more than the handful anyone can name. It includes obvious ones (©, —, …), mathematical operators, Greek letters, and a long tail of symbols nobody types by hand. Exactly 106 of those names are also accepted without the closing semicolon, purely so that pre-HTML5 documents keep rendering: &, <, >, ", ©,   and about a hundred more. Every other name requires the semicolon.
That legacy list is why a query string written as ?year=2026©=1 can render as ?year=2026©=1 in element text. It is also why the safe habit is not to think about which names are legacy: escape the ampersand as & and the question disappears. The same reasoning applies in reverse when reading someone else's markup — a stray © in the middle of a URL is a missing &, not a design decision.
For the five characters that matter, named references are the most readable and are what almost every library emits. For everything else, prefer the actual character in UTF-8. Numeric references earn their keep in exactly two places: the apostrophe, where ' avoids the ' compatibility question, and invisible characters — a zero-width joiner or a soft hyphen written as itself is invisible in the source too, so ­ is genuinely clearer than the character.
<!-- Three spellings of one character -->
<p>Tom & Jerry</p> <!-- named -->
<p>Tom & Jerry</p> <!-- decimal -->
<p>Tom & Jerry</p> <!-- hex -->
<!-- 106 of the 2,231 names also parse without the semicolon -->
<p>© 2026</p> <!-- © -->
<p>© 2026</p> <!-- © as well: "©" is one of the legacy names -->
<p>' done</p> <!-- ' — but "&apos" without the semicolon is not -->
<!-- Write & in URLs and none of the above can bite you -->
<a href="/search?q=cats&sort=new&copy=1">safe in every parser</a> and its relatives
is U+00A0 NO-BREAK SPACE, and it does two things: it renders as a space, and it forbids a line break at that point. It is the right tool for keeping 10 kg, Fig. 4 or Ctrl + C together on one line, and the wrong tool for everything else. Using a run of them for indentation or vertical rhythm produces text that reflows badly on a phone, breaks copy-paste into a spreadsheet, and defeats string comparison — a search for "Fig. 4" with an ordinary space will not match "Fig.\u00a04".
Its relatives are worth knowing because they show up in pasted content and cause bugs that look impossible. ­ (U+00AD) is a soft hyphen: invisible unless the line breaks there, at which point a hyphen appears. ‌ and ‍ (U+200C, U+200D) control ligature and emoji joining and are completely invisible.   and   are fixed-width spaces. Word processors and rich-text editors emit all of these freely, so any text arriving from a CMS is worth normalising before you compare it to anything.
The practical detection trick is that all of these are non-ASCII characters that look like ASCII. When a string comparison fails for no visible reason, or a JSON key mysteriously does not match, dump the code points rather than staring at the text. If the content is arriving as escaped markup and you need to see it plainly, an HTML escape and unescape tool will decode the references so you can see which invisible characters are really in there.
A minimal escape function, and why &amp; happens
A correct HTML escaper is five replacements and no more. Replace & first — if you replace it last, you will escape the ampersands you just introduced and produce &lt; where you wanted <. Doing all five in one regex pass, as below, makes the ordering problem impossible. This function is safe for element text and for quoted attribute values, and that is the whole of its remit; it is not a URL encoder, not a JavaScript escaper, and not a sanitiser.
Double escaping is the other failure mode, and it is the one users actually see. It happens when a value is escaped at two different layers: the template engine escapes it on the way out, and the application escaped it on the way in, or an API returns already-escaped HTML that a framework then escapes again. The symptom is &amp; on the page, or <b> showing as visible text where bold was intended, and every additional layer doubles the damage.
The fix is architectural rather than clever: store text unescaped, escape exactly once at the moment of output, and never escape on input. If you inherit a database full of &amp;, decoding it is straightforward but has to be done as many times as it was encoded — decoding &amp;amp; once gives &amp;, not &. The last two lines below show the shape of that, and the same round trip is what an escape and unescape tool performs when you paste a value into it.
const ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (ch) => ESCAPES[ch]);
}
escapeHtml('Tom & Jerry <b>"hi"</b>');
// 'Tom & Jerry <b>"hi"</b>'
escapeHtml("O'Reilly & Sons");
// 'O'Reilly & Sons'
escapeHtml('" onmouseover="alert(1)'); // an attribute break-out attempt
// '" onmouseover="alert(1)'
escapeHtml(escapeHtml('A & B')); // escaped twice: this is the bug
// 'A &amp; B'Frameworks escape for you — until they do not
In React, text children and interpolated expressions are inserted as DOM text nodes, not parsed as HTML, so a comment containing markup renders as the characters the user typed. Nothing you write as {value} in JSX can introduce a tag. Vue's mustache interpolation and Angular's binding syntax behave the same way. This is why modern applications have far fewer injection bugs than the PHP-and-string-concatenation era, and it is also why developers lose the habit of thinking about escaping at all.
Each framework then provides exactly one door out, and every real cross-site scripting bug in a modern codebase goes through one of them: dangerouslySetInnerHTML in React, v-html in Vue, bypassSecurityTrustHtml in Angular, the triple-mustache in Handlebars, the |safe filter in Jinja, and Element.innerHTML in plain DOM code. The names are deliberately alarming. Everything passed through them is parsed as markup, so it must already be safe — which in practice means it came from a sanitiser, not from an escaper.
The plain-DOM pair is the clearest illustration of the whole topic. Assigning to textContent sets the node's text directly, so the characters you assign are the characters shown and no reference is decoded. Assigning to innerHTML runs the HTML parser, so tags become elements and & becomes &. When you only want to display a string, textContent is both simpler and safer, and it is very often what innerHTML was being used for by accident.
const comment = 'Tom & Jerry <b>bold?</b>';
// React: inserted as text. The user sees the tags, they are not parsed.
<p>{comment}</p>
// The escape hatch. Everything inside must already be safe markup.
<p dangerouslySetInnerHTML={{ __html: sanitize(comment) }} />
// Plain DOM, the same distinction:
el.textContent = '& <b>x</b>'; // shows literally: & <b>x</b>
el.innerHTML = '& <b>x</b>'; // shows: & x (with x in bold)
// Decoding is not idempotent — each layer has to be undone separately.
decodeEntities('&amp;'); // '&'
decodeEntities('&'); // '&'Contexts where HTML escaping does not help
Escaping is not one operation; it is one operation per context, and using the wrong one is the same as using none. The clearest case is the script element. Because script is raw text, the parser never decodes character references inside it, so an escaper that turns < into < before writing into a script block corrupts the JavaScript without protecting it. Data going into a script needs JavaScript string escaping, plus special handling of the sequence </, which can terminate the element from inside a string literal.
URLs are the second case. A value interpolated into an href or src attribute is decoded by the HTML parser before the browser looks at the scheme, so javascript:alert(1) is javascript:alert(1) by the time it matters. HTML escaping does nothing about it. A URL value needs percent-encoding of its components — the job of URL encode and decode — followed by a scheme check that accepts only the schemes you intend, and only then HTML escaping to place it in the attribute.
Unquoted attributes are the third. If a template writes class=<value> with no quotes, a single space in the value starts a new attribute, and no amount of entity escaping prevents it, because a space is not one of the characters an HTML escaper touches. Quote every attribute value; it is the one rule that turns a whole class of injection into a non-issue. And XML has its own version of these rules — an XML escape tool applies the five predefined XML entities, which are similar but not identical to HTML's, since XML has no legacy names and no raw text elements.
| Context | Example | What works | What does not |
|---|---|---|---|
| Element text | <p>HERE</p> | HTML escaping of & and < | — |
| Quoted attribute | <a title="HERE"> | HTML escaping including the quote character | Leaving the quotes unescaped |
| Unquoted attribute | <a class=HERE> | Quote the attribute; then escape | Entity escaping on its own |
| URL attribute | <a href="HERE"> | Percent-encode, check the scheme, then escape | Entity escaping — javascript: still runs |
| Inside <script> | var x = "HERE" | JavaScript string escaping plus escaping </ | HTML entities — never decoded in raw text |
| Inside <style> | a { color: HERE } | CSS escaping, or an allow-list of values | HTML entities — style is raw text too |
<!-- Raw text: references are NOT decoded inside script or style -->
<script>
// "&" here stays five characters; it never becomes "&".
var label = "Tom & Jerry";
</script>
<!-- Escapable raw text: they ARE decoded inside textarea and title -->
<textarea>Tom & Jerry</textarea> <!-- the user sees: Tom & Jerry -->
<!-- URLs: the parser decodes the reference before the scheme is checked -->
<a href="javascript:alert(1)">this still runs</a>
<!-- Unquoted attributes: escaping entities does not save you -->
<a class=user-supplied onmouseover=alert(1)>quote your attributes</a>Sources
Frequently asked questions
Do I have to escape the greater-than sign?
Not strictly. The HTML syntax rules forbid only a literal < and an ambiguous ampersand in element text, so a lone > is valid data. Every escaping library escapes it anyway, which is the right default: it costs nothing, it keeps the output valid as XML, and it avoids arguments about edge cases such as the sequence ]]> inside embedded content.
Should I use ' or ' for an apostrophe?
Prefer '. The named form ' is defined in XML and in HTML5 but not in HTML 4, and it is not among the 106 legacy names that parse without a semicolon, so it has more ways to go wrong than the numeric form for no benefit. Escaping the apostrophe at all only matters inside single-quoted attribute values, but escapers usually do it unconditionally because they cannot know the template's quoting style.
Why does my page show &amp; instead of an ampersand?
The value was escaped twice — typically once when it was stored or returned by an API and again by the template engine on the way out. Each pass turns & into &, so & becomes &amp;. Fix it by storing text unescaped and escaping exactly once at output time; to repair existing data, decode as many times as it was encoded, because one decode of &amp; only gets you back to &.
Is escaping HTML enough to prevent XSS?
Only for the context it was designed for: element text and quoted attribute values. It does nothing inside a script or style element, where character references are never decoded; it does nothing about a javascript: URL in an href, because the parser decodes the reference before the scheme is checked; and it does nothing for an unquoted attribute, where a space alone breaks out. Each context needs its own escaping, and untrusted markup needs a sanitiser instead.
Do I still need é and friends in a UTF-8 page?
No. In a UTF-8 document any character can be written as itself, so accented letters, currency signs, dashes and arrows belong in the source as characters. Named references for them are a leftover from single-byte encodings; they still work but make the markup longer and unsearchable. Keep references for the five markup characters and for characters that are genuinely invisible, such as a soft hyphen.
Paste a snippet into the HTML Escape tool to convert markup characters into entities, or decode a value that has been escaped one time too many.
Escape or unescape HTMLKeep working
Related tools
HTML Escape
Convert text to safe HTML entities and back — <, >, & and quotes handled, named and numeric entities decoded.
HTML Formatter
Prettier-quality HTML formatting — embedded CSS and JavaScript formatted too, with a live preview one click away.
XML Escape
The five XML entities, handled both directions — make any text safe inside elements and attributes.
URL Encode / Decode
Percent-encoding both ways — make any text safe in a URL, or turn %20-soup back into readable text.