HTML Template Tutorial
In this tutorial you will build a small server-rendered website using the html tagged template. You will learn how to compose templates, safely handle user input, render lists, and return HTML responses from a router handler.
Prerequisites
Install Remix:
npm install remixAll imports come from a single path:
import { html, isSafeHtml } from 'remix/html-template'Step 1 --- Your First Template
The html tag returns a SafeHtml value. Convert it to a string with String() when you need raw HTML:
import { html } from 'remix/html-template'
let page = html`
<!DOCTYPE html>
<html lang="en">
<head><title>My Site</title></head>
<body>
<h1>Hello, World</h1>
</body>
</html>
`
console.log(String(page))Step 2 --- Interpolating Values Safely
Every interpolation is HTML-escaped. Try passing a string with angle brackets:
let userName = 'Alice <admin>'
let greeting = html`<p>Welcome, ${userName}!</p>`
// <p>Welcome, Alice <admin>!</p>This means you can safely interpolate any user-supplied value without worrying about XSS.
Step 3 --- Composing Templates
Break your page into reusable functions. Each function returns SafeHtml, and nested SafeHtml values are not double-escaped:
function layout(title: string, content: ReturnType<typeof html>) {
return html`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>${title}</title>
<style>
body { font-family: sans-serif; margin: 2rem; }
nav { margin-bottom: 1rem; }
nav a { margin-right: 1rem; }
</style>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
<main>${content}</main>
</body>
</html>
`
}
function homePage() {
return layout('Home', html`
<h1>Welcome</h1>
<p>This is the home page.</p>
`)
}
function aboutPage() {
return layout('About', html`
<h1>About Us</h1>
<p>We build things with Remix.</p>
`)
}Step 4 --- Rendering Lists
Map over arrays to produce lists. The html tag accepts arrays of SafeHtml and concatenates them:
interface Product {
name: string
price: number
}
function productTable(products: Product[]) {
let rows = products.map(
(p) => html`<tr><td>${p.name}</td><td>$${p.price.toFixed(2)}</td></tr>`,
)
return html`
<table>
<thead><tr><th>Product</th><th>Price</th></tr></thead>
<tbody>${rows}</tbody>
</table>
`
}Step 5 --- Handling User Input in Forms
When displaying form values back to the user (for example, after validation fails), the html tag escapes them automatically:
function contactForm(values: { name: string; message: string }, error?: string) {
return layout('Contact', html`
<h1>Contact Us</h1>
${error ? html`<p style="color: red;">${error}</p>` : html``}
<form method="POST" action="/contact">
<label>
Name
<input type="text" name="name" value="${values.name}" />
</label>
<label>
Message
<textarea name="message">${values.message}</textarea>
</label>
<button type="submit">Send</button>
</form>
`)
}Even if values.name contains "><script>alert(1)</script>, it will be escaped in the output.
Conditional rendering
Use the ternary operator with html for conditional blocks. For "nothing", use html\`` (an empty template).
Step 6 --- Returning HTML Responses
Integrate with a Remix router handler by converting the template to a Response:
import { html } from 'remix/html-template'
export function handleHome(request: Request): Response {
let page = layout('Home', html`
<h1>Welcome</h1>
<p>The time is ${new Date().toLocaleTimeString()}.</p>
`)
return new Response(String(page), {
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})
}
export async function handleContact(request: Request): Response {
if (request.method === 'POST') {
let formData = await request.formData()
let name = formData.get('name')?.toString() ?? ''
let message = formData.get('message')?.toString() ?? ''
if (!name || !message) {
let page = contactForm({ name, message }, 'All fields are required.')
return new Response(String(page), {
status: 422,
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})
}
// Process the message...
return Response.redirect('/contact?sent=1', 303)
}
let page = contactForm({ name: '', message: '' })
return new Response(String(page), {
headers: { 'Content-Type': 'text/html; charset=UTF-8' },
})
}What You Learned
- The
htmltag escapes all interpolated values to prevent XSS. SafeHtmlvalues nest without double-escaping, making composition natural.- Arrays of
SafeHtmlare concatenated automatically --- useful for lists. - Convert to a string with
String(page)and return it as aResponse. - Use
html.rawonly for trusted content (not covered here --- see the reference).
Related
- HTML Template Overview --- Feature summary and XSS prevention details.
- HTML Template Reference --- Full API reference.
- Component Overview --- For interactive UIs that need client-side rendering.