Skip to main content

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

npm i remix

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 &lt;script&gt;alert("XSS")&lt;/script&gt;!</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
Interpolation[]
required
Values to interpolate into the template (automatically escaped).
returns
SafeHtml
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
value
unknown
required
The value to check.
returns
boolean
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:
CharacterEscaped To
&&amp;
<&lt;
>&gt;
"&quot;
'&#39;

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

Always Escape User Input

// ✅ 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)