Skip to main content
The HTML Helper provides a tagged template literal for creating HTML strings with automatic XSS protection through HTML escaping.

Import

import { html, raw } from 'hono/html'

Functions

html()

Creates an HTML string with automatic escaping of interpolated values.
function html(
  strings: TemplateStringsArray,
  ...values: unknown[]
): HtmlEscapedString | Promise<HtmlEscapedString>
strings
TemplateStringsArray
required
The template string array
values
unknown[]
required
Values to interpolate into the template. All values are automatically HTML-escaped except those wrapped with raw().
return
HtmlEscapedString | Promise<HtmlEscapedString>
An HTML-escaped string. Returns a Promise if any interpolated value is a Promise.
Example
import { html } from 'hono/html'

app.get('/', (c) => {
  const name = 'John <script>alert("xss")</script>'
  
  // Values are automatically escaped
  const content = html`
    <html>
      <body>
        <h1>Hello ${name}!</h1>
      </body>
    </html>
  `
  // Output: <h1>Hello John &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;!</h1>
  
  return c.html(content)
})

Escaping Behavior

The html helper automatically escapes the following:
  • Strings: HTML special characters are escaped (<, >, &, ", ')
  • Numbers: Rendered as-is
  • Booleans, null, undefined: Ignored (rendered as empty string)
  • Arrays: Flattened with Array.flat(Infinity) and each element is processed
  • Promises: Awaited and then processed
  • Objects: Converted to string via .toString() and escaped
app.get('/demo', (c) => {
  const values = [
    'Name: John',
    ['<b>Bold</b>', undefined, null],
    ' Contact:',
    [html`<a href="/">Link</a>`]
  ]
  
  return c.html(html`<div>${values}</div>`)
  // Output: <div>Name: John&lt;b&gt;Bold&lt;/b&gt; Contact:<a href="/">Link</a></div>
})

Async Support

When interpolating Promises, the html function returns a Promise:
app.get('/async', async (c) => {
  const fetchUserName = async () => {
    // Fetch from database
    return 'John Doe'
  }
  
  const content = html`
    <html>
      <body>
        <h1>User: ${fetchUserName()}</h1>
      </body>
    </html>
  `
  
  // content is a Promise, so await it
  return c.html(await content)
})

raw()

Marks a string as safe HTML that should not be escaped.
function raw(value: string): HtmlEscapedString
value
string
required
The HTML string that should not be escaped
return
HtmlEscapedString
An HTML string marked as safe (not escaped)
Example
import { html, raw } from 'hono/html'

app.get('/with-raw', (c) => {
  const userInput = '<script>alert("xss")</script>'
  const safeHTML = '<strong>Important</strong>'
  
  return c.html(html`
    <div>
      <!-- This will be escaped -->
      <p>${userInput}</p>
      
      <!-- This will NOT be escaped -->
      <p>${raw(safeHTML)}</p>
    </div>
  `)
})
Output:
<div>
  <!-- Escaped -->
  <p>&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</p>
  
  <!-- Not escaped -->
  <p><strong>Important</strong></p>
</div>

Use Cases

Building Components

const Layout = (props: { title: string; children: any }) => html`
  <!DOCTYPE html>
  <html>
    <head>
      <title>${props.title}</title>
    </head>
    <body>
      ${props.children}
    </body>
  </html>
`

const Content = () => html`
  <div>
    <h1>Welcome</h1>
    <p>This is content</p>
  </div>
`

app.get('/', (c) => {
  return c.html(
    Layout({
      title: 'My Site',
      children: Content()
    })
  )
})

Conditional Rendering

app.get('/user/:id', (c) => {
  const user = getUser(c.req.param('id'))
  
  return c.html(html`
    <div>
      ${user
        ? html`<h1>Hello ${user.name}</h1>`
        : html`<p>User not found</p>`
      }
    </div>
  `)
})

Lists

app.get('/items', (c) => {
  const items = ['Apple', 'Banana', 'Cherry']
  
  return c.html(html`
    <ul>
      ${items.map(item => html`<li>${item}</li>`)}
    </ul>
  `)
})

Security

The html helper provides automatic XSS protection by escaping all interpolated values by default. Only use raw() when you are certain the content is safe.
// ✅ SAFE: User input is escaped
html`<div>${userInput}</div>`

// ⚠️ DANGEROUS: User input is not escaped
html`<div>${raw(userInput)}</div>`

// ✅ SAFE: Sanitized content
html`<div>${raw(sanitizeHTML(userInput))}</div>`

Types

interface HtmlEscapedString {
  isEscaped: true
  callbacks?: HtmlEscapedCallback[]
}

interface HtmlEscaped {
  isEscaped: true
}

Build docs developers (and LLMs) love