HTML escaping is one of those tasks that feels trivial until it isn't. Get it wrong and you're looking at broken layouts, mangled text, or — the fun one — a cross-site scripting hole that lets a stranger run JavaScript in your users' browsers. Get it right and nobody notices, which is exactly the point.
What escaping actually does
Browsers read certain characters as markup. A < starts a tag. An & starts an entity. If your data contains those characters and you drop them straight into the page, the browser interprets them instead of displaying them.
Escaping replaces those special characters with their entity equivalents so the browser renders them as literal text. The core set is small:
&becomes&<becomes<>becomes>"becomes"'becomes'(or')
The ampersand has to be escaped first, otherwise you'll double-escape your own output. More on that below.
When you actually need it
The rule of thumb: escape whenever untrusted or dynamic data lands in an HTML context. "Untrusted" doesn't only mean malicious users. It means anything you didn't hardcode — form input, database rows, API responses, filenames, even your own config values that someone might later change.
Common situations
- Rendering user comments, usernames, or profile bios in a page.
- Displaying search queries back to the user ("No results for
<script>"). - Showing code snippets or logs on a webpage.
- Injecting values into HTML attributes, like
title="..."orvalue="...".
If the content is going into HTML, escape it. The one exception is content you deliberately want to render as markup — and even then you should sanitise, not just trust it.
A worked example
Say a user types this into a comment box:
<script>alert('hacked')</script>If you concatenate that straight into your HTML:
document.getElementById('comments').innerHTML =
'<p>' + userComment + '</p>';...the browser sees a real <script> tag and runs it. Classic stored XSS. Escape it first and the same input becomes harmless text:
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
comments.innerHTML = '<p>' + escapeHtml(userComment) + '</p>';Now the page literally shows <script>alert('hacked')</script> as text. Notice the ampersand replacement comes first. Flip the order and < becomes <, then that ampersand gets re-escaped into &lt; — garbage.
Gotchas that bite people
1. Double-escaping
The most common bug. You escape a string, store it, then escape it again on the way out. Users see &lt; where they expected <. The fix is discipline: decide on one layer where escaping happens (usually at render time, in your templating engine) and never escape the same value twice. Store raw data, escape on output.
2. Context matters more than you think
HTML escaping is for HTML body content. It is not enough for every context:
- Inside a URL (
href,src) you need URL encoding. - Inside a
<script>block you need JavaScript string escaping. - Inside inline CSS you need CSS escaping.
Dropping HTML-escaped text into a JavaScript string won't protect you. Match the escaping to the destination.
3. Unquoted attributes
If you write attributes without quotes — value=<userdata> — then escaping < and > isn't enough, because a space or slash can break out of the attribute. Always quote your attributes and escape the quote character. This is cheap insurance.
4. Trusting your framework blindly
Modern frameworks auto-escape, which is great — React's {value}, Vue's {{ value }}, and most template engines all escape by default. But every one of them has an escape hatch: dangerouslySetInnerHTML, v-html, {{{ triple braces }}}. Those bypass escaping entirely. Know where they are in your codebase and treat them like loaded weapons.
5. Escaping is not sanitising
These solve different problems. Escaping turns markup into inert text. Sanitising removes dangerous parts while keeping some markup — what you'd use for a rich-text editor where users are allowed bold and links but not scripts. If you need to allow real HTML, reach for a dedicated sanitiser like DOMPurify, not a hand-rolled escape function.
Doing it by hand vs. reaching for a tool
For production code, lean on your framework's built-in escaping. Don't reinvent it. But there are plenty of moments when you just need to escape a chunk of text quickly — pasting a code sample into a blog post, debugging why a value renders oddly, or prepping some static HTML by hand. For those one-off jobs, our HTML Escape Tool handles it in the browser without you writing a throwaway script.
A quick mental checklist
- Store raw data. Escape at output time.
- Escape the ampersand first.
- Match the escaping to the context (HTML body, attribute, URL, JS, CSS).
- Quote your HTML attributes, always.
- Know where your framework's "unsafe" escape hatches are.
- If you need to keep some markup, sanitise — don't just escape.
None of this is complicated once the model clicks: the browser can't tell your data from your markup, so it's your job to draw the line. Escaping is how you draw it. Do it consistently, at the right layer, and most of the vulnerabilities in this category simply never appear.
Photo by Danial Igdery on Unsplash