Skip to Content
guidesJsx Templating

Last Updated: 3/9/2026


JSX Templating

Hono supports JSX for rendering HTML on the server. Write components with JSX syntax and render them as HTML responses.

Setup

Configure TypeScript to use Hono’s JSX runtime.

TypeScript Configuration

Add to your tsconfig.json:

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

For Deno, use deno.json instead:

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

File Extension

Rename your file from .ts to .tsx:

src/index.ts → src/index.tsx

Update your package.json scripts accordingly:

{ "scripts": { "dev": "bun run --hot src/index.tsx" } }

Basic Usage

Simple Component

import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => { return c.html( <html> <body> <h1>Hello Hono!</h1> </body> </html> ) }) export default app

Function Components

Create reusable components:

import type { FC } from 'hono/jsx' const Layout: FC = (props) => { return ( <html> <head> <title>My Site</title> </head> <body> {props.children} </body> </html> ) } const HomePage: FC = () => { return ( <Layout> <h1>Welcome</h1> <p>This is the home page</p> </Layout> ) } app.get('/', (c) => { return c.html(<HomePage />) })

Props and Types

Type-safe component props:

import type { FC } from 'hono/jsx' interface Post { id: number title: string body: string } const PostCard: FC<{ post: Post }> = ({ post }) => { return ( <article> <h2>{post.title}</h2> <p>{post.body}</p> </article> ) } const PostList: FC<{ posts: Post[] }> = ({ posts }) => { return ( <div> {posts.map((post) => ( <PostCard key={post.id} post={post} /> ))} </div> ) } app.get('/posts', async (c) => { const posts = await db.getPosts() return c.html(<PostList posts={posts} />) })

Children

PropsWithChildren

Type children properly:

import { PropsWithChildren } from 'hono/jsx' interface CardProps { title: string } const Card: FC<PropsWithChildren<CardProps>> = ({ title, children }) => { return ( <div class="card"> <h3>{title}</h3> <div class="card-body"> {children} </div> </div> ) } app.get('/', (c) => { return c.html( <Card title="Welcome"> <p>This is the card content</p> </Card> ) })

Fragments

Group elements without extra DOM nodes:

import { Fragment } from 'hono/jsx' const List: FC = () => { return ( <Fragment> <li>First</li> <li>Second</li> <li>Third</li> </Fragment> ) } // Or use shorthand const List2: FC = () => { return ( <> <li>First</li> <li>Second</li> <li>Third</li> </> ) }

Conditional Rendering

If/Else

const Greeting: FC<{ isLoggedIn: boolean; name?: string }> = ({ isLoggedIn, name, }) => { if (isLoggedIn) { return <p>Welcome back, {name}!</p> } return <p>Please log in</p> }

Ternary Operator

const Status: FC<{ isActive: boolean }> = ({ isActive }) => { return ( <div> <span>{isActive ? 'Active' : 'Inactive'}</span> </div> ) }

Logical AND

const Notification: FC<{ message?: string }> = ({ message }) => { return ( <div> {message && <div class="alert">{message}</div>} </div> ) }

Lists and Keys

Render arrays with keys:

const UserList: FC<{ users: User[] }> = ({ users }) => { return ( <ul> {users.map((user) => ( <li key={user.id}> {user.name} ({user.email}) </li> ))} </ul> ) }

Raw HTML

Insert raw HTML (use with caution):

app.get('/article', (c) => { const htmlContent = { __html: 'This &middot; is &middot; HTML' } return c.html( <div dangerouslySetInnerHTML={htmlContent} /> ) })

Warning: Only use dangerouslySetInnerHTML with trusted content to avoid XSS attacks.

Layouts and Renderers

Set Renderer

Define a layout once, use everywhere:

app.use(async (c, next) => { c.setRenderer((content) => { return c.html( <html> <head> <title>My App</title> <link rel="stylesheet" href="/styles.css" /> </head> <body> <header> <nav> <a href="/">Home</a> <a href="/about">About</a> </nav> </header> <main>{content}</main> <footer> <p>© 2024 My App</p> </footer> </body> </html> ) }) await next() }) app.get('/', (c) => { return c.render(<h1>Home Page</h1>) }) app.get('/about', (c) => { return c.render(<h1>About Page</h1>) })

Custom Renderer Props

Pass data to your layout:

declare module 'hono' { interface ContextRenderer { ( content: string | Promise<string>, props: { title: string; description?: string } ): Response | Promise<Response> } } app.use(async (c, next) => { c.setRenderer((content, props) => { return c.html( <html> <head> <title>{props.title}</title> {props.description && ( <meta name="description" content={props.description} /> )} </head> <body>{content}</body> </html> ) }) await next() }) app.get('/', (c) => { return c.render( <h1>Welcome</h1>, { title: 'Home - My App', description: 'Welcome to my app' } ) })

Metadata Hoisting

Metadata tags automatically move to <head>:

app.use(async (c, next) => { c.setRenderer((content) => { return c.html( <html> <head> {/* Metadata will be hoisted here */} </head> <body>{content}</body> </html> ) }) await next() }) app.get('/about', (c) => { return c.render( <> <title>About Page</title> <meta name="description" content="Learn about us" /> <h1>About</h1> <p>This is the about page</p> </> ) })

The <title> and <meta> tags automatically move to the <head> section.

Async Components

Components can be async:

const UserProfile: FC<{ userId: number }> = async ({ userId }) => { const user = await db.getUser(userId) return ( <div> <h2>{user.name}</h2> <p>{user.email}</p> </div> ) } app.get('/users/:id', async (c) => { const userId = parseInt(c.req.param('id')) return c.html( <html> <body> <UserProfile userId={userId} /> </body> </html> ) })

Suspense (Experimental)

Stream content with fallbacks:

import { renderToReadableStream, Suspense } from 'hono/jsx/streaming' const SlowComponent: FC = async () => { await new Promise((resolve) => setTimeout(resolve, 2000)) return <div>Loaded!</div> } app.get('/', (c) => { const stream = renderToReadableStream( <html> <body> <Suspense fallback={<div>Loading...</div>}> <SlowComponent /> </Suspense> </body> </html> ) return c.body(stream, { headers: { 'Content-Type': 'text/html; charset=UTF-8', 'Transfer-Encoding': 'chunked', }, }) })

The fallback renders immediately, then the component content streams when ready.

Error Boundaries (Experimental)

Catch errors in components:

import { ErrorBoundary } from 'hono/jsx' const BrokenComponent: FC = () => { throw new Error('Something went wrong') return <div>This won't render</div> } app.get('/', (c) => { return c.html( <html> <body> <ErrorBoundary fallback={<div>Error occurred</div>}> <BrokenComponent /> </ErrorBoundary> </body> </html> ) })

Context

Share data across component tree:

import { createContext, useContext } from 'hono/jsx' import type { FC } from 'hono/jsx' const ThemeContext = createContext('light') const Button: FC = () => { const theme = useContext(ThemeContext) return ( <button style={{ background: theme === 'dark' ? '#333' : '#fff' }}> Click me </button> ) } const Toolbar: FC = () => { return ( <div> <Button /> </div> ) } app.get('/', (c) => { return c.html( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ) })

Memoization

Optimize with memo:

import { memo } from 'hono/jsx' const Header = memo(() => ( <header> <h1>My Site</h1> <nav> <a href="/">Home</a> <a href="/about">About</a> </nav> </header> )) const Footer = memo(() => ( <footer> <p>© 2024 My Site</p> </footer> )) app.get('/', (c) => { return c.html( <div> <Header /> <main>Content here</main> <Footer /> </div> ) })

Integration with html Helper

Combine JSX with the html helper:

import { html } from 'hono/html' const Layout = (props: { title: string; children?: any }) => html`<!DOCTYPE html> <html> <head> <title>${props.title}</title> </head> <body> ${props.children} </body> </html>` const Content: FC<{ name: string }> = ({ name }) => ( <Layout title="Greeting"> <h1>Hello {name}!</h1> </Layout> ) app.get('/:name', (c) => { const name = c.req.param('name') return c.html(<Content name={name} />) })

JSX Renderer Middleware

Use the JSX Renderer middleware for cleaner code:

import { jsxRenderer } from 'hono/jsx-renderer' app.use( '*', jsxRenderer(({ children }) => { return ( <html> <head> <link rel="stylesheet" href="/styles.css" /> </head> <body>{children}</body> </html> ) }) ) app.get('/', (c) => { return c.render(<h1>Hello!</h1>) })

Custom Elements

Extend JSX with custom elements:

declare module 'hono/jsx' { namespace JSX { interface IntrinsicElements { 'my-button': HTMLAttributes & { variant?: 'primary' | 'secondary' size?: 'sm' | 'md' | 'lg' } } } } app.get('/', (c) => { return c.html( <my-button variant="primary" size="lg"> Click me </my-button> ) })

Best Practices

Keep Components Pure

Components should be pure functions:

// ✅ Good - pure function const Greeting: FC<{ name: string }> = ({ name }) => { return <h1>Hello {name}</h1> } // ❌ Bad - side effects const BadGreeting: FC<{ name: string }> = ({ name }) => { console.log('Rendering greeting') // Side effect! return <h1>Hello {name}</h1> }

Extract Reusable Components

// ✅ Good - reusable components const Button: FC<{ onClick?: string }> = ({ onClick, children }) => ( <button onclick={onClick}>{children}</button> ) const Card: FC<{ title: string }> = ({ title, children }) => ( <div class="card"> <h3>{title}</h3> {children} </div> ) // ❌ Bad - duplicated markup app.get('/page1', (c) => ( c.html( <div class="card"> <h3>Title</h3> <p>Content</p> </div> ) ))

Type Your Props

// ✅ Good - typed props interface UserCardProps { user: { id: number name: string email: string } } const UserCard: FC<UserCardProps> = ({ user }) => ( <div> <h2>{user.name}</h2> <p>{user.email}</p> </div> ) // ❌ Bad - untyped props const BadUserCard: FC = (props: any) => ( <div> <h2>{props.user.name}</h2> </div> )

Use Semantic HTML

// ✅ Good - semantic HTML const Article: FC = () => ( <article> <header> <h1>Title</h1> </header> <section> <p>Content</p> </section> </article> ) // ❌ Bad - div soup const BadArticle: FC = () => ( <div> <div> <div>Title</div> </div> <div> <div>Content</div> </div> </div> )

What’s Next?