html-template
The html-template package provides a tagged template literal for generating HTML with automatic escaping of interpolated values. It prevents XSS attacks by default and supports composing safe HTML fragments.
Installation
The html-template package is included with Remix. No additional installation is needed.
npm install remixOr install the standalone package:
npm install @remix-run/html-templateImport
import { html, isSafeHtml } from 'remix/html-template'Or from the standalone package:
import { html, isSafeHtml } from '@remix-run/html-template'API
html
A tagged template literal that escapes interpolated values as HTML. Returns a SafeHtml value.
let userInput = '<script>alert("xss")</script>'
let result = html`<h1>${userInput}</h1>`
String(result)
// '<h1><script>alert("xss")</script></h1>'Interpolations are escaped using the following rules:
| Character | Escaped As |
|---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
Interpolation Types
The html tag accepts these interpolation types:
| Type | Behavior |
|---|---|
string | HTML-escaped |
number | Converted to string, then HTML-escaped |
boolean | Converted to string, then HTML-escaped |
SafeHtml | Inserted as-is (not double-escaped) |
null / undefined | Produces empty string |
Array<Interpolation> | Each element processed recursively, then concatenated |
html.raw
A tagged template literal that does not escape interpolated values. Use only with trusted content or pre-escaped HTML.
let trustedIcon = '<svg>...</svg>'
let result = html.raw`<div class="icon">${trustedIcon}</div>`
String(result)
// '<div class="icon"><svg>...</svg></div>'WARNING
html.raw bypasses escaping. Only use it with content you trust completely. For user input, always use html (without .raw).
isSafeHtml(value)
Type guard that checks if a value is a SafeHtml instance.
import { html, isSafeHtml } from 'remix/html-template'
let safe = html`<p>Hello</p>`
let plain = '<p>Hello</p>'
isSafeHtml(safe) // true
isSafeHtml(plain) // falseTypes
SafeHtml
A branded String type that represents HTML content safe for rendering without further escaping. Created by the html and html.raw tagged templates.
import type { SafeHtml } from 'remix/html-template'SafeHtml extends String, so you can convert it with String(value) or use it in string contexts. The branding prevents accidental double-escaping when a SafeHtml value is interpolated into another html template.
Composing Templates
SafeHtml values are not double-escaped when nested inside another html template. This makes it natural to compose HTML from smaller pieces:
function renderHeader(title: string) {
return html`<header><h1>${title}</h1></header>`
}
function renderNav(items: string[]) {
let links = items.map((item) => html`<li>${item}</li>`)
return html`<nav><ul>${links}</ul></nav>`
}
function renderPage(title: string, navItems: string[]) {
return html`
<!DOCTYPE html>
<html>
<head><title>${title}</title></head>
<body>
${renderHeader(title)}
${renderNav(navItems)}
</body>
</html>
`
}
let page = renderPage('My Site', ['Home', 'About', 'Contact'])In this example:
titleand each nav item are escaped (they are plain strings).- The return values of
renderHeaderandrenderNavareSafeHtml, so they are inserted as-is.
Security Benefits
The html template tag prevents Cross-Site Scripting (XSS) by escaping all interpolated values by default:
// User-supplied input is always escaped
let comment = '<img src=x onerror=alert(1)>'
let safe = html`<div class="comment">${comment}</div>`
String(safe)
// '<div class="comment"><img src=x onerror=alert(1)></div>'This is safer than manual string concatenation or template literals without escaping:
// DANGEROUS: never do this with user input
let dangerous = `<div class="comment">${comment}</div>`Examples
Rendering a Response
import { html } from 'remix/html-template'
function handleRequest(request: Request): Response {
let page = html`
<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body>
<h1>Welcome</h1>
<p>The time is ${new Date().toLocaleTimeString()}</p>
</body>
</html>
`
return new Response(String(page), {
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})
}Dynamic Lists
import { html } from 'remix/html-template'
interface Book {
title: string
author: string
}
function renderBookList(books: Book[]) {
let rows = books.map(
(book) => html`<tr><td>${book.title}</td><td>${book.author}</td></tr>`,
)
return html`
<table>
<thead><tr><th>Title</th><th>Author</th></tr></thead>
<tbody>${rows}</tbody>
</table>
`
}Conditional Content
function renderAlert(message: string | null) {
if (!message) return html``
return html`<div class="alert">${message}</div>`
}
let page = html`
<main>
${renderAlert(errorMessage)}
<p>Content here</p>
</main>
`Related
- Components & JSX --- JSX-based component rendering (an alternative to template literals).
- Security --- Security best practices including XSS prevention.