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.tsxUpdate 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 appFunction 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 · is · 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?
- Add interactivity: Client-side JavaScript
- Style components: CSS and Styling
- Middleware reference: JSX Renderer Middleware