Ajo
Installation
Add Ajo to a project, configure the JSX runtime and render a component.


Ajo is a small library for building user interfaces with JSX and JavaScript generators. A component is either a plain function or a generator function, and Ajo updates the page in place, reusing the DOM nodes that are already there. This page sets up a project, configures JSX and renders a first pair of components.
Install the package
Ajo is a single package with no runtime dependencies.
pnpm add ajo@0.2.0You import from the first three entry points; your build tool imports the JSX runtime.
| Import | Contains |
|---|---|
ajo | render and the TypeScript types. |
ajo/context | context and current, for values shared down a tree. |
ajo/html | render, which turns components into HTML on a server. |
ajo/jsx-runtime | The automatic JSX runtime. ajo/jsx-dev-runtime exports the same functions. |
Configure JSX
Ajo uses the automatic JSX runtime: the compiler turns every JSX expression into a call to a function it imports from ajo/jsx-runtime on its own. Components never import anything to use JSX. Tell your tools that the JSX import source is ajo, in tsconfig.json.
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "ajo"
}
}TypeScript uses it to check JSX and to resolve element types in your editor, and Vite reads it for .ts and .tsx files. The value react-jsx is only TypeScript’s name for the automatic runtime mode; the import source decides which library provides it, and nothing here involves React.
For .jsx files, or Vite projects without a tsconfig, set oxc: { jsx: { importSource: 'ajo' } } in vite.config.ts. Other build tools take the same two settings: jsx set to react-jsx or automatic (the name depends on the tool) and jsxImportSource set to ajo.
Mount an application
Give the page an element to render into and load a module script:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Journal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>import { render } from 'ajo'
import { Journal } from './journal'
render(<Journal />, document.getElementById('root')!)The call render(children, container) reconciles the container’s child nodes with the JSX you pass: it reuses nodes that match, creates the missing ones and removes the rest. Mounting and updating are the same operation, so calling render again on the same container updates it in place, and a call with null empties the container and ends the components that were inside.
Two optional arguments limit the work to a range of the container’s children. With render(children, container, child, ref), Ajo starts at the node child and stops before the node ref, leaving everything outside the range untouched. This helps when a container also holds content Ajo does not manage, or when only part of some server-rendered HTML should be taken over.
import { render } from 'ajo'
import { Journal } from './journal'
const header = document.querySelector('body > header')!
const footer = document.querySelector('body > footer')!
// Reconcile only what lies between the static header and footer.
render(<Journal />, document.body, header.nextSibling, footer)A first pair of components
A stateless component is a function from its args to JSX. A stateful component is a generator function: variables declared before its loop hold the component’s state, and every pass through the loop yields what it shows. Calling this.next() applies a change and renders the component again.
import type { Stateful, Stateless } from 'ajo'
type EntryArgs = { title: string; date: string }
const Entry: Stateless<EntryArgs> = ({ title, date }) => (
<article class="entry">
<h2>{title}</h2>
<time datetime={date}>{date}</time>
</article>
)
export const Journal: Stateful = function* () {
const entries = [{ id: 1, title: 'First morning pages', date: '2026-09-21' }]
let title = ''
const add = () => this.next(() => {
if (!title.trim()) return
entries.unshift({ id: Date.now(), title, date: new Date().toISOString().slice(0, 10) })
title = ''
})
while (true) yield (
<>
<input
placeholder="Title of a new entry"
set:value={title}
set:oninput={e => title = (e.target as HTMLInputElement).value}
/>
<button set:onclick={add}>Add entry</button>
<ul>
{entries.map(entry => (
<li key={entry.id}>
<Entry title={entry.title} date={entry.date} />
</li>
))}
</ul>
</>
)
}Ajo wraps each stateful component in a host element, a <div> unless you choose another tag, and this inside the generator is that element. Typing into the input only updates the local title variable. The button calls this.next(), which runs the update and renders the list again. The key on each list item lets Ajo keep every entry’s DOM node when a new entry is added at the top.
With Ajo Kit
An Ajo Kit application comes set up for Ajo. Its project setup includes the JSX configuration, and Kit calls the render functions for you, on the server and in the browser, for every route. If you are building an application rather than adding Ajo to an existing page, start with Project setup.
- ComponentsArgs, the render loop, this.next() and the host element.
- RenderingAttributes, properties, events, keys and the special attributes.
- Server renderingRender the same components to HTML with ajo/html.
- Ajo KitTurn components into a routed application with server data.
Full reference: ajo LLMs.md