Skip to content

HTML Template Overview

The html-template package provides a single tagged template literal --- html --- that generates HTML with automatic escaping of all interpolated values. It is the simplest way to produce HTML in Remix V3 when you do not need a component system.

What is a tagged template literal?

A tagged template is a JavaScript feature where a function processes a template string and its interpolations. The html tag receives your template, escapes every interpolated value to prevent XSS, and returns a SafeHtml object.

Key Features

FeatureWhat It Does
Auto-escapingEvery interpolated value is HTML-escaped by default. <script> becomes &lt;script&gt;.
SafeHtml typeThe return type is SafeHtml, a branded string. When you nest one html result inside another, it is not double-escaped.
ComposableBuild pages from small template functions. Each returns SafeHtml and composes naturally.
No dependenciesZero runtime dependencies. Works in any JavaScript environment.

Quick Example

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

let userInput = '<script>alert("xss")</script>'

let page = html`
  <!DOCTYPE html>
  <html>
    <body>
      <h1>Hello</h1>
      <p>${userInput}</p>
    </body>
  </html>
`

// The script tag is escaped:
// <p>&lt;script&gt;alert("xss")&lt;/script&gt;</p>

XSS Prevention

Cross-Site Scripting (XSS) happens when untrusted input is inserted into HTML without escaping. The html tag prevents this automatically:

ts
// SAFE: html tag escapes the value
let safe = html`<div>${userComment}</div>`

// DANGEROUS: plain template literal does not escape
let dangerous = `<div>${userComment}</div>`

What is XSS?

Cross-Site Scripting (XSS) is an attack where a malicious user injects JavaScript into a web page that other users view. If a comment field allows <script> tags, an attacker can steal cookies or redirect users. The html tag escapes all dangerous characters so injected scripts are displayed as plain text.

The following characters are escaped:

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

Composing Templates

SafeHtml values are not double-escaped when nested. This makes it natural to break pages into functions:

ts
function renderHeader(title: string) {
  return html`<header><h1>${title}</h1></header>`
}

function renderPage(title: string, body: string) {
  return html`
    <!DOCTYPE html>
    <html>
      <head><title>${title}</title></head>
      <body>
        ${renderHeader(title)}
        <main>${body}</main>
      </body>
    </html>
  `
}

renderHeader returns SafeHtml, so it is inserted as-is into the outer template. The title and body strings are escaped.

The isSafeHtml Guard

Use isSafeHtml to check whether a value has already been escaped:

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

isSafeHtml(html`<p>safe</p>`)  // true
isSafeHtml('<p>plain</p>')      // false

When to Use html-template vs. component

Use CasePackage
Simple server-rendered pages, emails, RSS feedshtml-template
Interactive UIs, client hydration, animationscomponent
Mixing both in one appWorks fine --- html-template for static pages, component for interactive ones

Released under the MIT License.