Introduction

Estimated reading time: 4 minutes

Kita Html is a JSX runtime where every element evaluates to a string. Where React's <div /> produces a virtual DOM node that must be reconciled and serialized, Kita Html's <div /> returns '<div></div>' directly. The JSX you write compiles to function calls that concatenate strings.

This makes Kita Html faster than virtual DOM-based renderers whenever the output is an HTML string: server-side rendering, static site generation, email templates, HTMX applications, or any HTTP handler that sends HTML over the wire.

Go to getting started

Performance through simplicity

On each render, a virtual DOM renderer constructs an object tree, diffs it against the previous tree, and serializes the result to a string. Kita Html does none of this. The TypeScript compiler rewrites JSX expressions into jsx() and jsxs() function calls at build time. At runtime, those functions concatenate attribute strings and child strings into a single result. The output is the final HTML with no post-processing.

// What you write
let html: JSX.Element
html
= (
<
JSX.IntrinsicElements.div: JSX.HtmlTag
div
JSX.HtmlTag.class?: string | (string | number | boolean | null | undefined)[] | undefined

The html class property. You can use an array to represent conditional class names. Similar to the clsx package behavior.

@example
<div class={['a', true && 'b', false && 'c', 'd']} />
'<div class="a b d"></div>'

<div class={['class-a class-b', true && 'class-c']} />
'<div class="class-a class-b class-c"></div>'
class
="card">
<
JSX.IntrinsicElements.h1: JSX.HtmlTag
h1
JSX.HtmlTag.safe?: boolean | undefined

When set to true, all inner content (html or not) of this tag will be escaped when evaluated.

Warning: This will escape even inner jsx tags. You should only use this in the most inner tag of the html tree.

@example
<div>{'<script />'}</div>
'<div><script /></div>'
<div safe>{'<script />'}</div>
'<div><script /></div>'
<div><div>{'<script />'}</div></div>
'<div><div><script /></div></div>'

// Escapes even inner jsx tags
<div safe><div>{'<script />'}</div></div>
'<div><div><script /></div></div>'
@defaultfalse@seehttps ://html.kitajs.org/guide/xss/safe-attribute
safe
>{
const username: string
username
}</
JSX.IntrinsicElements.h1: JSX.HtmlTag
h1
>
</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>
) // What TypeScript compiles to
let html: JSX.Element
html
=
function __jsx(this: void, name: string | Function, attributes: {
    children?: Children;
    [k: string]: any;
}): JSX.Element

Generates a html string from an attribute name of component and it's props.

This function is meant to be used by the jsx runtime and should not be called directly.

@paramname The name of the element to create or another component function@paramattributes The props to apply to the component@returnsThe generated html string or a promise that resolves to the generated html string
__jsx
('div', {
class: string
class
: 'card',
children?: Children
children
:
function __jsx(this: void, name: string | Function, attributes: {
    children?: Children;
    [k: string]: any;
}): JSX.Element

Generates a html string from an attribute name of component and it's props.

This function is meant to be used by the jsx runtime and should not be called directly.

@paramname The name of the element to create or another component function@paramattributes The props to apply to the component@returnsThe generated html string or a promise that resolves to the generated html string
__jsx
('h1', {
safe: boolean
safe
: true,
children?: Children
children
:
const username: string
username
}) }) // Click on the <> icon in the top right to see the compiled output
<div class="card"><h1>Username</h1></div>

In the RealWorldPage benchmark, Kita Html renders about 2-3x faster than React, Preact, and HonoJsx. The only dependency is csstype, which provides types for the style attribute.

Read more about compilation and rendering

XSS protection

Because JSX.Element is a plain string (or a promise of one), the runtime cannot distinguish between HTML produced by a component and raw user input injected as a child. Children are not escaped by default. This is a deliberate trade-off for performance and composability. The safe attribute and the kita-html check CLI close that gap.

The safe attribute escapes children at render time. Adding it to any native element causes the runtime to pass all children through HTML entity escaping before concatenation.

const 
const html: JSX.Element
html
= <
JSX.IntrinsicElements.div: JSX.HtmlTag
div
JSX.HtmlTag.safe?: boolean | undefined

When set to true, all inner content (html or not) of this tag will be escaped when evaluated.

Warning: This will escape even inner jsx tags. You should only use this in the most inner tag of the html tree.

@example
<div>{'<script />'}</div>
'<div><script /></div>'
<div safe>{'<script />'}</div>
'<div><script /></div>'
<div><div>{'<script />'}</div></div>
'<div><div><script /></div></div>'

// Escapes even inner jsx tags
<div safe><div>{'<script />'}</div></div>
'<div><div><script /></div></div>'
@defaultfalse@seehttps ://html.kitajs.org/guide/xss/safe-attribute
safe
>{
let userInput: "<script>alert('XSS')</script>"
userInput
}</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>
<div>&lt;script>alert(&#39;XSS&#39;)&lt;/script></div>

kita-html check uses the TypeScript 7.1 type checker to analyze every JSX expression and flags any child whose type could carry unescaped HTML. It runs in CI/CD pipelines, failing the build on any finding, and can feed editor problem panels. Values that bypass the type system, such as as 'safe' casts or safe-prefixed names, are not checked.

Read more about the XSS protection system

Async components and Suspense

Kita Html supports async components natively. Any function component that returns a Promise<string> is valid JSX. When an async child appears anywhere in the tree, the entire parent chain becomes a Promise<string> through automatic propagation.

async function 
function UserCard({ id }: {
    id: string;
}): Promise<string>
UserCard
({
id: string
id
}: {
id: string
id
: string }) {
const
const user: {
    name: string;
}
user
= await
const db: {
    getUser(id: string): Promise<{
        name: string;
    }>;
}
db
.
function getUser(id: string): Promise<{
    name: string;
}>
getUser
(
id: string
id
)
return <
JSX.IntrinsicElements.div: JSX.HtmlTag
div
JSX.HtmlTag.safe?: boolean | undefined

When set to true, all inner content (html or not) of this tag will be escaped when evaluated.

Warning: This will escape even inner jsx tags. You should only use this in the most inner tag of the html tree.

@example
<div>{'<script />'}</div>
'<div><script /></div>'
<div safe>{'<script />'}</div>
'<div><script /></div>'
<div><div>{'<script />'}</div></div>
'<div><div><script /></div></div>'

// Escapes even inner jsx tags
<div safe><div>{'<script />'}</div></div>
'<div><div><script /></div></div>'
@defaultfalse@seehttps ://html.kitajs.org/guide/xss/safe-attribute
safe
>{
const user: {
    name: string;
}
user
.
name: string
name
}</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>
} // The result is a Promise<string> because UserCard is async. // Its type is still JSX.Element, so it can be used in JSX. const
const html: JSX.Element
html
= <
function UserCard({ id }: {
    id: string;
}): Promise<string>
UserCard
id: string
id
="123" />
<div>Arthur</div>

For streaming scenarios, the Suspense component renders a fallback immediately and replaces it with the resolved content as each async subtree completes. This uses HTTP chunked transfer encoding to stream updates to the client without waiting for the entire page to resolve. A small inline script handles the client-side DOM replacement.

const 
const stream: Readable
stream
=
function renderToStream(html: JSX.Element | ((rid: number | string) => JSX.Element), rid?: number | string): Readable

Transforms a component tree who may contain Suspense components into a stream of HTML.

@example
import { text} from 'node:stream/consumers';

// Prints out the rendered stream (2nd example shows with a custom id)
const stream = renderToStream(r => <AppWithSuspense rid={r} />)
const stream = renderToStream(<AppWithSuspense rid={myCustomId} />, myCustomId)

// You can consume it as a stream
for await (const html of stream) {
 console.log(html.toString())
}

// Or join it all together (Wastes ALL Suspense benefits, but useful for testing)
console.log(await text(stream))
@paramhtml The component tree to render or a function that returns the component tree.@paramrid The request id to identify the request, if not provided, a new incrementing id will be used.@see{@linkcode Suspense}
renderToStream
((
rid: string | number
rid
) => (
<
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
SuspenseProps.rid: string | number

The request id is used to identify the request for this suspense.

rid
={
rid: string | number
rid
}
SuspenseProps.fallback: JSX.Element

The fallback to render while the async children are loading.

fallback
={<
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>Loading...</
JSX.IntrinsicElements.div: JSX.HtmlTag
div
>}>
<
function UserCard({ id }: {
    id: string;
}): Promise<string>
UserCard
id: string
id
="123" />
</
function Suspense(props: SuspenseProps): JSX.Element

A component that returns a fallback while the async children are loading.

The rid prop is the one {@linkcode renderToStream } returns, this way the suspense knows which request it belongs to.

Warning: Using Suspense without any type of runtime support will LEAK memory and not work. Always use with renderToStream or within a framework that supports it.

Suspense
>
)) // Pipe the stream to your HTTP response or use one of our integrations.
<div id="B:4:1" data-sf><div>Loading...</div></div>
<!-- SuspenseScript omitted for brevity -->
<template id="N:4:1" data-sr><div>Async Component</div></template>
<script id="S:4:1" data-ss>
  $KITA_RC("4:1");
</script>

Each Suspense boundary operates independently, so fast components appear instantly while slow ones show their fallback. The rid parameter ties each Suspense instance to a specific request for safe concurrent rendering.

Read more about Suspense streaming

Packages

The main packages are:

  • @kitajs/html is the core runtime that renders JSX to strings and provides Suspense streaming.
  • @kitajs/html-cli provides kita-html check, the XSS scanner for TypeScript 7.1 and later.
  • @kitajs/ts-html-plugin is the deprecated editor plugin and xss-scan CLI for TypeScript 6.

Official framework integrations are available for Fastify and Express, and Elysia provides its own. The core runtime works with any framework that accepts strings.

Read more about the packages and integrations

Built something interesting with Kita Html? Show us where you're using it.