A deliberately tiny signals UI runtime. Real nodes, created once, mutated forever by signals — no virtual DOM, no diffing, no re-render.
A UI runtime small enough to read in an afternoon, built on alien-signals. It ships in two shapes:
| Package | What it renders to |
|---|---|
@amritk/mini |
The DOM. Reactive bindings, keyed lists, static-template cloning, and a compilerless JSX runtime. |
@amritk/mini-native |
Whatever a pluggable Host puts in front of it — a native view tree (Lynx), the DOM as a preview target, or plain objects. |
Both are compilerless: there is no custom build step, just the standard
react-jsx transform pointed at the package's own runtime.
JSX builds a real node once. Signals mutate it forever. Nothing diffs.
const Counter = () => {
const count = signal(0)
return (
<button onclick={() => count(count() + 1)}>
{() => `clicked ${count()} times`}
</button>
)
}The component function runs a single time. The <button> is created once
and never rebuilt; only the text node inside it is ever written to again.
Nothing analyses your expressions, so reactivity is decided by value shape at runtime:
- A function-valued attribute, child, or
showis reactive. - Any other value is static — applied once, never again.
Signals are zero-argument functions, so passing one without calling it is already a live binding:
<button disabled={streaming}> {/* reactive — tracks forever */}
<button disabled={streaming()}> {/* STATIC — frozen at creation! */}That second line is the one real footgun, so it is guarded three ways: live in
the editor and dev server through @amritk/mini/vite, in this repo's CI through
bun run check:reactivity, and in your own CI through the same plugin.
There is no virtual DOM, no diffing, and no re-render — and that is a commitment, not a roadmap item. It buys two things:
- The
.entry stays tiny and flat. Every feature only some apps need lives on its own subpath with its own module graph, so importing/routerpulls in none of/forms. A size-budget test and an import-boundary test hold that line in CI, per package. - Porting is one file. The hard part of a React-style native framework is
the reconciler; there is none here to port. A new target for
@amritk/mini-nativemeans implementing oneHost— about 15 functions — and nothing else.
If a feature seems to be missing, the correct next step is usually a bigger framework (Preact, Solid), not a new helper here.
npm install @amritk/mini
# or: pnpm add / yarn add / bun addimport { mount, signal } from '@amritk/mini'
mount(document.body, Counter)Full API, subpath modules, and the native runtime's host contract are in the package READMEs:
@amritk/mini— DOM bindings,list,template, and the/router/flow/forms/query/hot/vitesubpaths.@amritk/mini-native— theHostcontract, the native element vocabulary, and the three shipped hosts.
Each package ships an AI.md next to its README.md: a mental model, a
minimal runnable example, and the gotchas most likely to trip up a model. The
repo root carries llms.txt (a curated index, per
llmstxt.org) and llms-full.txt
(every AI.md inlined, for pasting into context).
Editing the repo rather than consuming it? Start at AGENTS.md,
then the per-package AGENTS.md for the invariants that package cannot break.
- Consumers: any ES2022 runtime.
@amritk/minineeds a DOM;@amritk/mini-nativeneeds only a host. - Development: Bun ≥ 1.1.
bun install
bun run test # every package's tests
bun run check # biome lint + format
bun run check:reactivity # the called-signal footgun guard
bun run types:check # both packages, both of mini-native's passes
bun run build # build both packages
bun run test:dist # load and drive the built artifacts (needs a build)See CONTRIBUTING.md for the full workflow, and
.claude/architecture.md for how the two packages
relate.
Pre-alpha. Breaking changes can land in any 0.x release; they ride a minor bump.
@amritk/mini/forms can validate against a JSON Schema through
@amritk/runtime-validators, an optional
peer published from the mjst repo, where both
packages used to live.