Skip to Content

Last Updated: 3/7/2026


JSX

You can write HTML with JSX syntax with hono/jsx.

Although hono/jsx works on the client, you will probably use it most often when rendering content on the server side. Here are some things related to JSX that are common to both server and client.

Settings

To use JSX, modify the tsconfig.json:

{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "hono/jsx" } }

Alternatively, use the pragma directives:

/** @jsx jsx */ /** @jsxImportSource hono/jsx */

For Deno, you have to modify the deno.json instead of the tsconfig.json:

{ "compilerOptions": { "jsx": "precompile", "jsxImportSource": "@hono/hono/jsx" } }

Usage

INFO: If you are coming straight from the Quick Start, the main file has a .ts extension - you need to change it to .tsx - otherwise you will not be able to run the application at all.

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: { messages: string[] }) => { return ( <Layout> <h1>Hello Hono!</h1> <ul> {props.messages.map((message) => { return <li>{message}!!</li> })} </ul> </Layout> ) } app.get('/', (c) => { const messages = ['Good Morning', 'Good Evening', 'Good Night'] return c.html(<Top messages={messages} />) }) export default app

Metadata hoisting

You can write document metadata tags such as <title>, <meta>, and <link> directly inside your components. These tags will be automatically hoisted to the <head> section of the document.

import { Hono } from 'hono' const app = new Hono() 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='This is the about page.' /> <p>About page content</p> </> ) }) export default app

INFO: When hoisting occurs, existing elements are not removed. Elements appearing later are added to the end.

Fragment

Use Fragment to group multiple elements without adding extra nodes:

import { Fragment } from 'hono/jsx' const List = () => ( <Fragment> <p>first child</p> <p>second child</p> <p>third child</p> </Fragment> )

Or you can write it with <>>:

const List = () => ( <> <p>first child</p> <p>second child</p> <p>third child</p> </> )

PropsWithChildren

You can use PropsWithChildren to correctly infer a child element in a function component.

import { PropsWithChildren } from 'hono/jsx' type Post = { id: number title: string } function Component({ title, children }: PropsWithChildren<Post>) { return ( <div> <h1>{title}</h1> {children} </div> ) }

Inserting Raw HTML

To directly insert HTML, use dangerouslySetInnerHTML:

app.get('/foo', (c) => { const inner = { __html: 'JSX · SSR' } const Div = <div dangerouslySetInnerHTML={inner} /> })

Memoization

Optimize your components by memoizing computed strings using memo:

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

By using useContext, you can share data globally across any level of the Component tree without passing values through props.

import type { FC } from 'hono/jsx' import { createContext, useContext } 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> } const Toolbar: FC = () => { return ( <div> <Button /> </div> ) } app.get('/', (c) => { return c.html( <div> <ThemeContext.Provider value={themes.dark}> <Toolbar /> </ThemeContext.Provider> </div> ) })

Async Component

hono/jsx supports an Async Component, so you can use async/await in your component. If you render it with c.html(), it will await automatically.

const AsyncComponent = async () => { await new Promise((r) => setTimeout(r, 1000)) // sleep 1s return <div>Done!</div> } app.get('/', (c) => { return c.html( <html> <body> <AsyncComponent /> </body> </html> ) })

Suspense (Experimental)

The React-like Suspense feature is available. If you wrap the async component with Suspense, the content in the fallback will be rendered first, and once the Promise is resolved, the awaited content will be displayed.

import { renderToReadableStream, Suspense } from 'hono/jsx/streaming' app.get('/', (c) => { const stream = renderToReadableStream( <html> <body> <Suspense fallback={<div>loading...</div>}> <Component /> </Suspense> </body> </html> ) return c.body(stream, { headers: { 'Content-Type': 'text/html; charset=UTF-8', 'Transfer-Encoding': 'chunked', }, }) })

ErrorBoundary (Experimental)

You can catch errors in child components using ErrorBoundary.

function SyncComponent() { throw new Error('Error') return <div>Hello</div> } app.get('/sync', async (c) => { return c.html( <html> <body> <ErrorBoundary fallback={<div>Out of Service</div>}> <SyncComponent /> </ErrorBoundary> </body> </html> ) })

ErrorBoundary can also be used with async components and Suspense.

async function AsyncComponent() { await new Promise((resolve) => setTimeout(resolve, 2000)) throw new Error('Error') return <div>Hello</div> } app.get('/with-suspense', async (c) => { return c.html( <html> <body> <ErrorBoundary fallback={<div>Out of Service</div>}> <Suspense fallback={<div>Loading...</div>}> <AsyncComponent /> </Suspense> </ErrorBoundary> </body> </html> ) })

Integration with html Middleware

Combine the JSX and html middlewares for powerful templating.

import { Hono } from 'hono' import { html } from 'hono/html' const app = new Hono() interface SiteData { title: string children?: any } const Layout = (props: SiteData) => html` <!DOCTYPE html> <html> <head> <title>${props.title}</title> </head> <body> ${props.children} </body> </html> ` const Content = (props: { siteData: SiteData; name: string }) => ( <Layout {...props.siteData}> <h1>Hello {props.name}</h1> </Layout> ) app.get('/:name', (c) => { const { name } = c.req.param() const props = { name: name, siteData: { title: 'JSX with html sample', }, } return c.html(<Content {...props} />) }) export default app