Documentation Index
Fetch the complete documentation index at: https://mintlify.com/remix-run/remix/llms.txt
Use this file to discover all available pages before exploring further.
Safe HTML template literals for Remix that automatically escape interpolated values to prevent XSS while supporting explicit trusted HTML insertion.
Installation
Features
- Automatic HTML Escaping - All interpolated values are escaped by default
- Explicit Raw HTML - Use
html.raw for trusted HTML sources
- Composable - SafeHtml values can be nested without double-escaping
- Type-Safe - Full TypeScript support with branded types
- Zero Dependencies - Lightweight and self-contained
- Runtime Agnostic - Works in Node.js, Bun, Deno, browsers, and edge runtimes
Basic Usage
import { html } from 'remix/html-template'
let userInput = '<script>alert("XSS")</script>'
let greeting = html`<h1>Hello ${userInput}!</h1>`
console.log(String(greeting))
// Output: <h1>Hello <script>alert("XSS")</script>!</h1>
All interpolated values are automatically escaped to prevent XSS attacks.
API Reference
html
A tagged template function that escapes interpolated values as HTML.
function html(
strings: TemplateStringsArray,
...values: Interpolation[]
): SafeHtml
strings
TemplateStringsArray
required
The template strings from the tagged template literal.
Values to interpolate into the template (automatically escaped).
A branded string that is safe to render as HTML.
html.raw
A tagged template function that does NOT escape interpolated values. Use only with trusted content.
function html.raw(
strings: TemplateStringsArray,
...values: Interpolation[]
): SafeHtml
Only use html.raw with content you trust. Never use it with user input as it can lead to XSS vulnerabilities.
import { html } from 'remix/html-template'
let trustedIcon = '<svg>...</svg>'
let button = html.raw`<button>${trustedIcon} Click me</button>`
console.log(String(button))
// Output: <button><svg>...</svg> Click me</button>
isSafeHtml
Checks if a value is a SafeHtml string.
function isSafeHtml(value: unknown): value is SafeHtml
true if the value is a SafeHtml string, false otherwise.
import { html, isSafeHtml } from 'remix/html-template'
let safe = html`<div>Safe</div>`
let unsafe = '<div>Unsafe</div>'
isSafeHtml(safe) // true
isSafeHtml(unsafe) // false
Examples
Composing HTML Fragments
SafeHtml values can be nested without double-escaping:
import { html } from 'remix/html-template'
let title = html`<h1>My Title</h1>`
let content = html`<p>Some content with ${userInput}</p>`
let page = html`
<!doctype html>
<html>
<body>
${title}
${content}
</body>
</html>
`
Working with Arrays
Interpolate arrays of values, which will be flattened and joined:
import { html } from 'remix/html-template'
let items = ['Apple', 'Banana', 'Cherry']
let list = html`
<ul>
${items.map((item) => html`<li>${item}</li>`)}
</ul>
`
Conditional Rendering
Use null or undefined to render nothing:
import { html } from 'remix/html-template'
let showError = false
let errorMessage = 'Something went wrong'
let page = html`
<div>
${showError ? html`<div class="error">${errorMessage}</div>` : null}
</div>
`
Building Complete Pages
import { html } from 'remix/html-template'
function renderPage(title: string, content: string) {
return html`
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${title}</title>
</head>
<body>
<main>${content}</main>
</body>
</html>
`
}
let userContent = '<script>alert("XSS")</script>'
let page = renderPage('My Page', userContent)
// userContent is automatically escaped
Using with Response
Create HTML responses using SafeHtml:
import { html } from 'remix/html-template'
import { createHtmlResponse } from 'remix/response/html'
let page = html`
<h1>Welcome</h1>
<p>Hello, ${userName}!</p>
`
let response = createHtmlResponse(page)
Mixing Safe and Raw HTML
import { html } from 'remix/html-template'
let userBio = '<script>alert("XSS")</script>'
let adminContent = '<strong>Admin Notice</strong>' // trusted content
let page = html`
<div class="user-profile">
<h2>User Bio</h2>
<div class="bio">${userBio}</div>
<div class="admin-notice">
${html.raw`${adminContent}`}
</div>
</div>
`
// userBio is escaped, adminContent is not
Type Definitions
type SafeHtml = String & { readonly [kSafeHtml]: true }
type Interpolation =
| SafeHtml
| string
| number
| boolean
| null
| undefined
| Array<Interpolation>
interface SafeHtmlHelper {
(strings: TemplateStringsArray, ...values: Interpolation[]): SafeHtml
raw(strings: TemplateStringsArray, ...values: Interpolation[]): SafeHtml
}
const html: SafeHtmlHelper
function isSafeHtml(value: unknown): value is SafeHtml
Escaping Rules
The following characters are escaped when using html:
| Character | Escaped To |
|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
Value Handling
Automatic Escaping (html)
- Strings: HTML-escaped
- Numbers/Booleans: Converted to string and escaped
- SafeHtml: Used as-is (no double-escaping)
- Arrays: Recursively processed and joined
- null/undefined: Rendered as empty string
Raw Interpolation (html.raw)
- Strings: Used as-is (NOT escaped)
- Numbers/Booleans: Converted to string (not escaped)
- SafeHtml: Used as-is
- Arrays: Recursively processed and joined
- null/undefined: Rendered as empty string
Best Practices
// ✅ Good: User input is automatically escaped
let page = html`<div>${userInput}</div>`
// ❌ Bad: User input is NOT escaped
let page = html.raw`<div>${userInput}</div>`
Use html.raw Only for Trusted Content
// ✅ Good: Using raw for static, trusted HTML
let icon = '<svg><path d="..."/></svg>'
let button = html.raw`<button>${icon} Click</button>`
// ❌ Bad: Using raw with user content
let comment = getUserComment() // Could contain malicious HTML
let post = html.raw`<div>${comment}</div>`
Compose SafeHtml Values
// ✅ Good: Compose using SafeHtml
function userCard(user: User) {
return html`
<div class="card">
<h3>${user.name}</h3>
<p>${user.bio}</p>
</div>
`
}
let cards = users.map((user) => userCard(user))
let page = html`<div class="users">${cards}</div>`
Convert to String When Needed
let page = html`<h1>Hello</h1>`
// Explicit conversion
String(page)
// Implicit conversion (in string contexts)
console.log(page) // Automatically converts to string
response.headers.set('Content-Length', String(page.length))
- response - Response helpers (includes
createHtmlResponse that works with SafeHtml)