Skip to Content

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 &middot; 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> ) })