Skip to content

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.

bash
npm install remix

Or install the standalone package:

bash
npm install @remix-run/html-template

Import

ts
import { html, isSafeHtml } from 'remix/html-template'

Or from the standalone package:

ts
import { html, isSafeHtml } from '@remix-run/html-template'

API

html

A tagged template literal that escapes interpolated values as HTML. Returns a SafeHtml value.

ts
let userInput = '<script>alert("xss")</script>'
let result = html`<h1>${userInput}</h1>`

String(result)
// '<h1>&lt;script&gt;alert("xss")&lt;/script&gt;</h1>'

Interpolations are escaped using the following rules:

CharacterEscaped As
&&amp;
<&lt;
>&gt;
"&quot;
'&#39;

Interpolation Types

The html tag accepts these interpolation types:

TypeBehavior
stringHTML-escaped
numberConverted to string, then HTML-escaped
booleanConverted to string, then HTML-escaped
SafeHtmlInserted as-is (not double-escaped)
null / undefinedProduces 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.

ts
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.

ts
import { html, isSafeHtml } from 'remix/html-template'

let safe = html`<p>Hello</p>`
let plain = '<p>Hello</p>'

isSafeHtml(safe)   // true
isSafeHtml(plain)  // false

Types

SafeHtml

A branded String type that represents HTML content safe for rendering without further escaping. Created by the html and html.raw tagged templates.

ts
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:

ts
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:

  • title and each nav item are escaped (they are plain strings).
  • The return values of renderHeader and renderNav are SafeHtml, so they are inserted as-is.

Security Benefits

The html template tag prevents Cross-Site Scripting (XSS) by escaping all interpolated values by default:

ts
// 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">&lt;img src=x onerror=alert(1)&gt;</div>'

This is safer than manual string concatenation or template literals without escaping:

ts
// DANGEROUS: never do this with user input
let dangerous = `<div class="comment">${comment}</div>`

Examples

Rendering a Response

ts
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

ts
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

ts
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>
`

  • Components & JSX --- JSX-based component rendering (an alternative to template literals).
  • Security --- Security best practices including XSS prevention.

Released under the MIT License.