Last Updated: 3/11/2026
JSX
Write HTML with JSX syntax using hono/jsx for server-side rendering.
Configuration
Modify tsconfig.json:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}For Deno, modify deno.json:
{
"compilerOptions": {
"jsx": "precompile",
"jsxImportSource": "@hono/hono/jsx"
}
}Basic Usage
Rename your file to .tsx and use JSX:
import { Hono } from 'hono'
import type { FC } from 'hono/jsx'
const app = new Hono()
const Layout: FC = (props) => {
return (
<html>
<body>{props.children}</body>
</html>
)
}
const Top: FC<{ messages: string[] }> = (props) => {
return (
<Layout>
<h1>Hello Hono!</h1>
<ul>
{props.messages.map((message) => (
<li>{message}!!</li>
))}
</ul>
</Layout>
)
}
app.get('/', (c) => {
const messages = ['Good Morning', 'Good Evening', 'Good Night']
return c.html(<Top messages={messages} />)
})Metadata Hoisting
Metadata tags are automatically hoisted to <head>:
app.use('*', async (c, next) => {
c.setRenderer((content) => {
return c.html(
<html>
<head></head>
<body>{content}</body>
</html>
)
})
await next()
})
app.get('/about', (c) => {
return c.render(
<>
<title>About Page</title>
<meta name='description' content='About page description' />
<p>About page content</p>
</>
)
})Fragments
import { Fragment } from 'hono/jsx'
const List = () => (
<Fragment>
<p>first child</p>
<p>second child</p>
</Fragment>
)
// Or use shorthand
const List2 = () => (
<>
<p>first child</p>
<p>second child</p>
</>
)Props with Children
import { PropsWithChildren } from 'hono/jsx'
type Post = {
id: number
title: string
}
function Component({ title, children }: PropsWithChildren<Post>) {
return (
<div>
<h1>{title}</h1>
{children}
</div>
)
}Raw HTML
app.get('/foo', (c) => {
const inner = { __html: 'JSX · SSR' }
return c.html(<div dangerouslySetInnerHTML={inner} />)
})Memoization
import { memo } from 'hono/jsx'
const Header = memo(() => <header>Welcome to Hono</header>)
const Footer = memo(() => <footer>Powered by Hono</footer>)
const Layout = (
<div>
<Header />
<p>Hono is cool!</p>
<Footer />
</div>
)Context
Share data across components:
import { createContext, useContext } from 'hono/jsx'
import type { FC } from 'hono/jsx'
const themes = {
light: { color: '#000000', background: '#eeeeee' },
dark: { color: '#ffffff', background: '#222222' },
}
const ThemeContext = createContext(themes.light)
const Button: FC = () => {
const theme = useContext(ThemeContext)
return <button style={theme}>Push!</button>
}
app.get('/', (c) => {
return c.html(
<ThemeContext.Provider value={themes.dark}>
<Button />
</ThemeContext.Provider>
)
})Async Components
const AsyncComponent = async () => {
await new Promise((r) => setTimeout(r, 1000))
return <div>Done!</div>
}
app.get('/', (c) => {
return c.html(
<html>
<body>
<AsyncComponent />
</body>
</html>
)
})Suspense (Experimental)
import { renderToReadableStream, Suspense } from 'hono/jsx/streaming'
app.get('/', (c) => {
const stream = renderToReadableStream(
<html>
<body>
<Suspense fallback={<div>loading...</div>}>
<AsyncComponent />
</Suspense>
</body>
</html>
)
return c.body(stream, {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
'Transfer-Encoding': 'chunked',
},
})
})Error Boundary (Experimental)
import { ErrorBoundary } from 'hono/jsx/streaming'
function Component() {
throw new Error('Error')
}
app.get('/', (c) => {
return c.html(
<ErrorBoundary fallback={<div>Out of Service</div>}>
<Component />
</ErrorBoundary>
)
})