Using this package? Please consider donating to support my open source work ❤️
Help @kitajs/html grow! Star and share this amazing repository with your friends and co-workers!
🏛️ KitaJS Html
@kitajs/html
is a no dependencies, fast and concise package to generate HTML through JavaScript with JSX syntax.
Preview
Installing
To use the @kitajs/html
package, follow these steps:
-
Install the required npm packages, @kitajs/html
and @kitajs/ts-html-plugin
, to
enable editor intellisense. Open your terminal and run:
npm install @kitajs/html @kitajs/ts-html-plugin
-
Configure your TypeScript project to transpile TSX/JSX into JavaScript and using our
LSP Plugin. Update your tsconfig.json
file with
the following settings:
// tsconfig.json
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "Html.createElement",
"jsxFragmentFactory": "Html.Fragment",
"plugins": [{ "name": "@kitajs/ts-html-plugin" }]
}
}
-
Append the
xss-scan
command
into your test script. This CLI comes from @kitajs/ts-html-plugin, which catches XSS
vulnerabilities that may be introduced into your codebase. Open your package.json
file and add the following script:
// package.json
{
"scripts": {
"test": "xss-scan"
}
}
-
Ensure that your code editor is using the TypeScript version from your project's
node_modules
instead of the globally installed TypeScript. For Visual Studio Code,
you can configure this in your workspace settings:
// .vscode/settings.json
{
"typescript.tsdk": "node_modules/typescript/lib"
}
[!WARNING]
Make sure to verify that your setup is correct by writing
console.log(<div>{'<' + '/div>'}</div>);
in your editor. If it PRODUCE ERRORS,
your setup is correct. Refer to the
@kitajs/ts-html-plugin repository for more
details on setting up editor intellisense.
Getting Started
After successfully installing and configuring your project, you can start using JSX syntax
to generate HTML. Here are two options for importing the @kitajs/html
package:
-
Import it manually: Import the package in your TypeScript files where you need it,
avoiding global scope pollution.
console.log(<div>Html import needs to be in scope!</div>);
-
Use the register to add a global namespace: Import the register
to globally
register all necessary functions for convenience.
import '@kitajs/html/register';
console.log(<div>It works without importing!</div>);
Now you can use JSX to generate HTML throughout your project. Always use the safe
attribute or manually call Html.escapeHtml
to protect against XSS vulnerabilities when
rendering user input.
Ensuring XSS prevention is vital to guarantee your application's security. You can employ
the @kitajs/ts-html-plugin
to catch XSS
issues in your code editor and enhance your code's security.
Sanitization
[!IMPORTANT]
Please utilize our @kitajs/ts-html-plugin
to emit TypeScript errors wherever you are
exposed to XSS. Refer to Getting Started for installation
instructions.
This package sanitizes every attribute by default. However, since the resulting element is
always a string, it's impossible to differentiate an HTML element created by a <tag>
or
from user input. This necessitates the use of the provided safe
or manual invocation of Html.escapeHtml
.
<div>⚠️ This will NOT be escaped and WILL expose you to XSS</div>
<div attr="This WILL be escaped"></div>
<div safe>This WILL be escaped</div>
<div>{Html.escapeHtml('This WILL be escaped')}</div>
Here's an example of how this is DANGEROUS for your application:
user = {
name: 'Bad guy',
description: '</div><script>getStoredPasswordAndSentToBadGuysServer()</script>'
}
<div class="user-card">{user.description}</div>
<div class="user-card">
<script>getStoredPasswordAndSentToBadGuysServer()</script>
</div>
<div class="user-card" safe>{user.description}</div>
<div class="user-card">
</div><script>getStoredPasswordAndSentToBadGuysServer()</script>
</div>
The Safe Attribute
Always use the safe
attribute when rendering uncontrolled user input. This will sanitize
the contents and prevent 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 you should only use the safe
attribute at the very bottom of the HTML tree
where it's needed.
Editor Intellisense and CLI tool
⚠️
Note: This section has been relocated to the
@kitajs/ts-html-plugin repository.
Please consult their
"Getting Started" section for
instructions on enabling editor IntelliSense and using the CLI tool.
Async Components
Async components are supported. When any child or sub child of a component tree is a
promise, the whole tree will return a promise of the html string.
If no async components are found, the result will be simply a string, and you can safely
cast it into a string.
async function Async() {
await callApi();
return <div>Async!</div>;
}
function Sync() {
return <div>Sync!</div>;
}
const async = (
<div>
<Async />
</div>
);
async instanceof Promise;
const sync: string = (
<div>
<Sync />
</div>
);
typeof sync === 'string';
A JSX.Element
will always be a string. Once a children element is a async component, the
entire upper tree will also be async.
Learn when JSX.Element is a Promise.
Suspense component
The only problem when rendering templates is that you must wait for the whole template to
be rendered before sending it to the client. This is not a problem for small templates,
but it can be a problem for large templates.
To solve this problem, we provide a Suspense
component that combined with
renderToStream()
rendering method, will stream a fallback component while it waits for
his children to be rendered. This is a perfect combo to use with
async components.
import { Suspense, renderToStream } from '@kitajs/html/suspense';
async function MyAsyncComponent() {
const data = await database.query();
return <User name={data.username} />;
}
function renderUserPage(rid: number) {
return (
<Suspense
rid={rid}
fallback={<div>Loading username...</div>}
catch={(err) => <div>Error: {err.stack}</div>}
>
<MyAsyncComponent />
</Suspense>
);
}
const html = renderToStream(renderUserPage);
[!NOTE]
The renderToStream()
is returns a native node/bun stream, head over to our
suspense-server example to see how to use it with
node:http, Express or Fastify servers.
The above example would render <div>Loading username...</div>
while waiting for the
MyAsyncComponent
to be rendered.
When using Suspense
, you cannot just call the component and get the html string, you
need to use the renderToStream
function to get a stream that can be piped to the client
with updates. Otherwise, the fallback would render forever.
As the result of any JSX component is always a string, you must use the rid
provided by
renderToStream
into all your suspense components, this way we can identify which
suspense is for which request and be able to render concurrent requests.
Suspense also accepts async fallbacks, but it blocks rendering until the fallback is
resolved.
function renderTemplate(rid: number) {
return (
<Suspense
rid={rid}
fallback={<MyAsyncFallback />}
catch={(err) => <div>Error: {err.stack}</div>}
>
<MyAsyncComponent />
</Suspense>
);
}
The above example would only return anything after MyAsyncFallback
is resolved. To catch
async fallback errors, you must wrap it into a ErrorBoundary
.
Error boundaries
The same way as promises must be awaited to resolve its own html, errors must be caught.
Outside of suspense components, you can use the provided error boundaries to catch errors.
import { ErrorBoundary } from '@kitajs/html/error-boundary';
async function MyAsyncComponent() {
const data = await database.query();
return <User name={data.username} />;
}
function renderTemplate() {
return (
<ErrorBoundary catch={(err) => <div>Error: {err.stack}</div>}>
<MyAsyncComponent />
</ErrorBoundary>
);
}
const html = await renderTemplate();
Error boundaries will only work for errors thrown inside async components, for sync
components you must use try/catch.
function MySyncComponent() {
try {
const data = database.query();
return <User name={data.username} />;
} catch (err) {
return <div>Error: {err.stack}</div>;
}
}
Error boundaries outside suspense components will only catch errors thrown by the fallback
component. You must use the Suspense's catch
property to handle errors thrown by its
children components.
function renderTemplate(rid: number) {
return (
<ErrorBoundary catch={<div>fallback error</div>}>
<Suspense
rid={rid}
fallback={<MyAsyncFallback />}
catch={<div>Children error</div>}
>
<MyAsyncComponent />
</Suspense>
</ErrorBoundary>
);
}
const html = renderToStream(renderTemplate);
The above example would render <div>Children error</div>
if MyAsyncComponent
throws an
error, or <div>fallback error</div>
if MyAsyncFallback
throws an error. If both throws
an error, the first error will be changed to the second error as soon as the children
error is thrown.
Why JSX.Element is a Promise?
[!NOTE]
Until #14729 gets implemented,
you need to manually cast JSX.Element
into strings if you are sure there is no inner
async components in your component tree.
JSX elements are mostly strings everywhere. However, as the nature of this package, once a
children element is a async component, the entire upper tree will also be async. Unless
you are sure that no other component in your entire codebase is async, you should always
handle both string and promise cases.
const html = <Layout />;
if (html instanceof Promise) {
console.log(await html);
} else {
console.log(html);
}
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.
You can use Html To Jsx.
<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>
Results into:
<>
{}
<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>
</>
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 add this triple slash directive to the top of your file:
import '@kitajs/html/register';
const html = (
<div hx-get="/api" hx-trigger="click" hx-target="#target">
Click me!
</div>
);
Hotwire Turbo
This project supports the usage of Turbo Hotwire. We
provide a separate export that you can use to provide type definitions for the elements
and attributes used with Turbo Hotwire.
You just need to add this triple slash directive to the top of your file:
import '@kitajs/html/register';
const html = (
<turbo-frame id="messages">
<a href="/messages/expanded">Show all expanded messages in this frame.</a>
<form action="/messages">Show response from this form within this frame.</form>
</turbo-frame>
);
Base HTML templates
Often you will have a "template" html with doctype, things on the head, body and so on...
Most users try to use them as a raw string and only use JSX for other components, but this
is a not a good idea as
you will have problems with it.
But you can always concatenate strings, like in this required use-case for <doctype>
export function Layout(props: Html.PropsWithChildren<{ head: string; title?: string }>) {
return (
<>
{'<!doctype html>'}
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{props.title || 'Hello World!'}</title>
{props.head}
</head>
<body>{props.children}</body>
</html>
</>
);
}
const html = (
<Layout
head={
<>
<link rel="stylesheet" href="/style.css" />
<script src="/script.js" />
</>
}
>
<div>Hello World</div>
</Layout>
);
Compiling HTML
Html.compile
interface compiles a clean component into a super fast
component. This does not support unclean components / props processing.
[!WARNING]
This feature is a special use case for rendering entire page templates like what you
would do with handlebars or nunjucks.
It does not works with mostly JSX components and, for small components,
it will be slower than the normal JSX syntax.
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!
Due to the nature of
Proxy
objects, the spread operator (...
) will not work with compiled components. You need to
manually pass all props to their components.
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<{ sum: number }>) {
return <div>{props.sum}</div>;
}
html = <Clean sum={3 * 2} />;
function Unclean(props: { a: number; b: number }) {
return <div>{props.a * props.b}</div>;
}
html = <Unclean a={3} b={2} />;
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.
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.
declare global {
namespace JSX {
interface IntrinsicElements {
mathPower: HtmlTag & {
['my-exponential']: number;
children: number;
};
}
interface HtmlTag {
['hx-boost']: boolean;
}
}
}
const element = (
<mathPower my-exponential={2} hx-boost>
{3}
</mathPower>
);
Allow everything!
We also provide a way to allow any tag/attribute combination, altough we do not
recommend using it.
Just add this triple slash directive to the top of your file:
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 benchmark below ran with a 13600k
@5.1GHz machine.
# Benchmark
- 2023-10-07T16:32:38.524Z
- Node: v20.8.0
- V8: 11.3.244.8-node.16
- OS: linux
- Arch: x64
## Hello World
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html |
| ------ | ------------ | ---------- | ----- | ---------- | ---------------- | -------------- |
| 10 | 0.0051ms | 0.0115ms | 2.25x | 0.037ms | 0.14x | 0.31x |
| 10000 | 0.5453ms | 1.8617ms | 3.41x | 0.2964ms | 1.84x | 6.28x |
| 100000 | 4.2937ms | 12.4456ms | 2.9x | 1.3948ms | 3.08x | 8.92x |
## Mdn Homepage
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html |
| ------ | ------------ | ------------ | ----- | ---------- | ---------------- | -------------- |
| 10 | 0.6555ms | 3.8335ms | 5.85x | 0.0216ms | 30.37x | 177.62x |
| 10000 | 564.8611ms | 2972.7084ms | 5.26x | 0.2644ms | 2136.01x | 11241.23x |
| 100000 | 5682.3627ms | 30272.9508ms | 5.33x | 1.4159ms | 4013.37x | 21381.35x |
## Many Props
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html |
| ------ | ------------ | ---------- | ----- | ---------- | ---------------- | -------------- |
| 10 | 0.1863ms | 0.9531ms | 5.12x | 0.0107ms | 17.38x | 88.93x |
| 10000 | 165.5015ms | 705.8572ms | 4.26x | 0.6545ms | 252.85x | 1078.41x |
| 100000 | 1669.4264ms | 7092.11ms | 4.25x | 3.0786ms | 542.27x | 2303.68x |
## Many Components
| Runs | @kitajs/html | typed-html | + | .compile() | + / @kitajs/html | + / typed-html |
| ------ | ------------ | ----------- | ----- | ---------- | ---------------- | -------------- |
| 10 | 0.1567ms | 0.3688ms | 2.35x | 0.0027ms | 57.43x | 135.17x |
| 10000 | 145.0478ms | 362.2195ms | 2.5x | 1.2776ms | 113.53x | 283.52x |
| 100000 | 1400.2911ms | 3676.7311ms | 2.63x | 5.7281ms | 244.46x | 641.88x |
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 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.