@kitajs/html
is a no dependencies, fast and concise package to generate HTML through JavaScript with JSX syntax.
Table of Contents
Installing
npm install @kitajs/html
Getting Started
Install @kitajs/html
with your favorite package manager, import it into the top of your jsx
/tsx
file and change your tsconfig.json to transpile jsx syntax.
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment"
}
}
import '@kitajs/html/register'
import Html from '@kitajs/html'
console.log(<div>Hello World</div>)
function route(request, response) {
return response
.header('Content-Type', 'text/html')
.send(<div>Hello World</div>)
}
fs.writeFileSync(
'index.html',
<html>
<head>
<title>Hello World</title>
</head>
<body>
<div>Hello World</div>
</body>
</html>
)
function Layout({ name, children }: Html.PropsWithChildren<{ name: string }>) {
return (
<html>
<head>
<title>Hello World</title>
</head>
<body>
<div>Hello {name}</div>
{children}
</body>
</html>
)
}
console.log(<Layout name="World">I'm in the body!</Layout>)
typeof (<div>Hello World</div>) === 'string'
This package just provides functions to transpile JSX to a HTML string, you can imagine doing something like this before, but now with type checking and intellisense:
const html = `<div> Hello World!<div>` ❌
const html = <div>Hello World!<div> ✅
Sanitization
This package aims to be a HTML builder, not an HTML sanitizer. This means that no HTML content is escaped by default. However we provide a custom attribute called safe
that will sanitize everything inside of it. You can also use the exported Html.escapeHtml
function to escape other contents arbitrarily.
<div style={'"&<>\''}></div> // <div style=""&<>'"></div>
<div style={{ backgroundColor: '"&<>\'' }}></div>
<div safe>{untrusted}</div> // <div><script>alert("hacked!")</script></div>
<div>{'<a></a>' + Html.escapeHtml('<a></a>')}</div> // <div><a></a><a></a></div>
<div>{untrusted}</div> // <div><script>alert('hacked!')</script></div>
It's like if React's dangerouslySetInnerHTML
was enabled by default.
The safe attribute
You should always use the safe
attribute when you are rendering user input. This will sanitize its contents and avoid XSS attacks.
function UserCard({ name, description, date, about }) {
return (
<div class="card">
<h1 safe>{name}</h1>
<br />
<p safe>{description}</p>
<br />
// controlled input, no need to sanitize
<time datetime={date.toISOString()}>{date.toDateString()}</time>
<br />
<p safe>{about}</p>
</div>
)
}
Note that only at the very bottom of the HTML tree is where you should use the safe
attribute, to only escape where its needed.
👉 There's an open issue to integrate this within a typescript plugin to emit warnings and alerts to use the safe attribute everywhere a variable is used. Wanna help? Check this issue.
Migrating from HTML
Migrating from plain HTML to JSX can be a pain to convert it all manually, as you will find yourself hand placing quotes and closing void elements. Luckily for us, there's a tool called htmltojsx that can help us with that.
<div class="awesome" style="border: 1px solid red">
<label for="name">Enter your name: </label>
<input type="text" id="name" />
</div>
<p>Enter your HTML here</p>
Generates:
<>
{}
<div className="awesome" style={{ border: '1px solid red' }}>
<label htmlFor="name">Enter your name: </label>
<input type="text" id="name" />
</div>
<p>Enter your HTML here</p>
</>
Base HTML templates
Often you will have a "template" html with doctype, things on the head, body and so on... The layout is also a very good component to be compiled. Here is a effective example on how to do it:.
export const Layout = html.compile(
(p: Html.PropsWithChildren<{ head: string }>) => (
<>
{'<!doctype html>'}
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Document</title>
{p.head}
</head>
<body>{p.children}</body>
</html>
</>
)
)
const html = (
<Layout
head={
<>
<link rel="stylesheet" href="/style.css" />
<script src="/script.js" />
</>
}>
<div>Hello World</div>
</Layout>
)
Htmx
The usage of htmx.org is super common with this project, this is why we also provide type definitions for all HTMX attributes.
You just need to import '@kitajs/html/htmx'
at the top of your main.ts file and you will be able to use all HTMX attributes.
import '@kitajs/html/htmx'
import '@kitajs/html/register'
const html = (
<div hx-get="/api" hx-trigger="click" hx-target="#target">
Click me!
</div>
)
It can also be included in your tsconfig.json
to enable intellisense and type checking for all HTMX attributes.
{
"compilerOptions": {
"types": ["node_modules/@kitajs/html/htmx"]
}
}
Or with a triple slash directive:
Compiling HTML
Compiles a clean component into a super fast component. This does not
support unclean components / props processing.
This mode works just like prepared statements in SQL. Compiled components can give up to 2000 times faster html generation. This is a opt-in feature that you may not be able to use everywhere!
import Html from '@kitajs/html'
function Component(props: Html.PropsWithChildren<{ name: string }>) {
return <div>Hello {props.name}</div>
}
const compiled = Html.compile<typeof Component>(Component)
compiled({ name: 'World' })
const compiled = Html.compile((p) => <div>Hello {p.name}</div>)
compiled({ name: 'World' })
Properties passed for compiled components ARE NOT what will be passed as argument to the generated function.
const compiled = Html.compile((t) => {
console.log(t.asd)
return <div></div>
})
compiled({ asd: 123 })
That's the reason on why you cannot compile unclean components, as they need to process the props before rendering.
Clean Components
A clean component is a component that does not process props before
applying them to the element. This means that the props are applied to the
element as is, and you need to process them before passing them to the
component.
function Clean(props: PropsWithChildren<{ repeated: string }>) {
return <div>{props.repeated}</div>
}
html = <Clean name={'a'.repeat(5)} />
function Unclean(props: { repeat: string; n: number }) {
return <div>{props.repeat.repeat(props.n)}</div>
}
html = <Unclean repeat="a" n={5} />
Fragments
JSX does not allow multiple root elements, but you can use a fragment to group multiple elements:
const html = (
<>
<div>1</div>
<div>2</div>
</>
)
Learn more about JSX syntax here!
Supported HTML
All HTML elements and attributes should be supported.
Missing an element or attribute? Please create an issue or a PR to add it. It's easy to add.
The tag
tag
The <tag of="">
tag is a custom internal tag that allows you to render any runtime selected tag you want. Possibly reasons to prefer this tag over extending types:
- You want to render a tag that is chosen at runtime.
- You don't want to mess up with extending globally available types.
- You are writing javascript with typechecking enabled.
- You are writing a library and should not extend types globally.
- You need to use kebab-case tags, which JSX syntax does not support.
<tag of="asd" />
<tag of="my-custom-KEBAB" />
We do recommend using extending types instead, as it will give you intellisense and type checking.
Async Components
Sadly, we cannot allow async components in JSX and keep the same string type for everything else. Even though it should be possible to write async components you will have no real benefit from it, as you will always have to await the whole html generation
to complete before you can render it.
You should fetch async data in the following way:
async function render(name) {
const data = await api.data(name)
const otherData = await api.otherData(name)
return <Layout data={data} otherData={data} />
}
Extending types
Just as exemplified above, you may also want to add custom properties to your elements. You can do this by extending the JSX
namespace.
⚠️ Please follow the JSX convention and do not use kebab-case
for your properties, use camelCase
instead. We internally transform all camelCase
properties to kebab-case
to be compliant with the HTML and JSX standards.
declare global {
namespace JSX {
interface IntrinsicElements {
mathPower: HtmlTag & {
myExponential: number
children: number
}
}
interface HtmlTag {
hxBoost: boolean
}
}
}
const element = (
<mathPower myExponential={2} hxBoost>
{3}
</mathPower>
)
Performance
This package is just a string builder on steroids, as you can see how this works. This means that most way to isolate performance differences is to micro benchmark.
You can run this yourself by running pnpm bench
. The bench below was with a Apple M1 Pro 8gb.
# Benchmark
- 2023-09-11T06:33:17.036Z
- Node: v20.6.0
- V8: 10.2.154.26-node.26
- OS: darwin
- Arch: arm64
## Hello World
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html |
| ------ | ------------ | ---------- | ----- | ---------- | ---------------- | -------------- |
| 10 | 0.0111ms | 0.0149ms | 1.34x | 0.0071ms | 1.56x | 2.1x |
| 10000 | 1.389ms | 4.3509ms | 3.13x | 0.5962ms | 2.33x | 7.3x |
| 100000 | 9.6386ms | 25.7452ms | 2.67x | 2.6383ms | 3.65x | 9.76x |
## Many Props
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html |
| ------ | ------------ | ------------ | ----- | ---------- | ---------------- | -------------- |
| 10 | 0.8359ms | 2.4209ms | 2.9x | 0.0041ms | 203.88x | 590.46x |
| 10000 | 654.4648ms | 1696.169ms | 2.59x | 1.0713ms | 610.91x | 1583.28x |
| 100000 | 6022.4039ms | 13676.7311ms | 2.27x | 4.9011ms | 1228.79x | 2790.54x |
## Big Component
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html |
| ------ | ------------ | ----------- | ----- | ---------- | ---------------- | -------------- |
| 10 | 0.4776ms | 1.0979ms | 2.3x | 0.0052ms | 91.85x | 211.13x |
| 10000 | 411.6375ms | 987.0599ms | 2.4x | 1.2778ms | 322.15x | 772.47x |
| 100000 | 3986.7891ms | 9863.4924ms | 2.47x | 6.9333ms | 575.02x | 1422.63x |
How it works
This package just aims to be a drop in replacement syntax for JSX, and it works because you tell tsc to transpile JSX syntax to calls to our own html
namespace.
<ol start={2}>
{[1, 2].map((i) => (
<li>{i}</li>
))}
</ol>
Gets transpiled by tsc to plain javascript:
Html.createElement(
'ol',
{ start: 2 },
[1, 2].map((i) => Html.createElement('li', null, i))
)
Which, when called, returns this string:
'<ol start="2"><li>1</li><li>2</li></ol>'
Format HTML output
This package emits HTML as a compact string, useful for over the wire environments. However, if your use case really needs the output
HTML to be pretty printed, you can use an external JS library to do so, like html-prettify.
import Html from '@kitajs/html'
import prettify from 'html-prettify'
const html = (
<div>
<div>1</div>
<div>2</div>
</div>
)
console.log(html)
console.log(prettify(html))
👉 There's an open PR to implement this feature natively, wanna work on it? Check this PR.
Fork credits
This repository was initially forked from typed-html and modified to add some features and increase performance.
Initial credits to nicojs and contributors for the amazing work.
Licensed under the Apache License, Version 2.0.