The smallest cut.
Introducing Kerf. The smallest cut.
~11 KB. No virtual DOM. No compiler. No magic. Reactive UI that touches only the bytes that changed.
import { signal, mount } from 'kerfjs';
const count = signal(0);
mount(document.getElementById('app')!, () => (
<div>
<button data-action="inc">+</button>
<span>{count.value}</span>
</div>
));That's it. Your JSX renders to HTML strings, kerf's native diff applies the minimum DOM mutations to make the live tree match, and signals re-run the render only when something they read actually changed.
-
Small bundle. ~11 KB minified + gzipped including
@preact/signals-core(~12 KB witharraySignal). One runtime dependency. No virtual DOM, no scheduler, no concurrent-mode machinery. On the krausest js-framework-benchmark kerf is in the same cluster as Vue, vanjs, and Lit on most operations; Solid wins the compiler-drivenselect rowandpartial updatebenchmarks. -
No virtual DOM, no compiler. JSX → HTML strings → native diff. DevTools shows the real DOM because it is the DOM.
-
Focus, selection, listeners survive re-renders. The reconciler morphs instead of rebuilding — caret position, selection range, and delegated listeners survive every re-render.
-
Small public API. ~16 exports total. No hooks, no lifecycle, no per-instance state. Components are plain functions that return JSX.
-
Plain TS, plain JSX, plain ESM. Drops into anything using esbuild / Vite / tsup. No plugin chain.
- Hybrid desktop apps (Tauri / Electron) — small bundle, predictable diff, debuggable runtime; ideal for the embedded webview.
- Embedded widgets — chat bubbles, comment boxes, dashboards dropped into someone else's page.
- Server-rendered apps with islands — Rails / Phoenix / Django / Hono.
mountper island;delegatesurvives turbo-frame swaps. - Admin panels & internal tools — reactivity without 200 KB of framework + state lib + router.
- Replacing jQuery — incremental migration; same delegation mental model, modern primitives.
- Prototyping — entire mental model on a postcard.
- Need a full ecosystem (router + forms + data + SSR streaming) → Next.js / Remix / SolidStart.
- Building a deeply componentised design-system app → React / Solid / Svelte.
- Need React Native / cross-platform mobile → React (Kerf + Tauri/Electron also covers many of these cases).
- Building a static site → Astro (we use it for this project's site).
- Already invested in a framework where switching cost outweighs the bundle size gain.
import { signal, computed, effect, defineStore, mount, each, delegate } from 'kerfjs';
// 1. A signal — single piece of reactive state.
const count = signal(0);
// 2. A computed — auto-derived from other signals.
const doubled = computed(() => count.value * 2);
// 3. A store — multi-consumer state with named actions and reset semantics.
const cart = defineStore({
initial: () => ({ items: [] as { id: string; name: string }[] }),
actions: (set, get) => ({
add: (id: string, name: string) => set({ items: [...get().items, { id, name }] }),
remove: (id: string) => set({ items: get().items.filter((i) => i.id !== id) }),
}),
});
// 4. Mount JSX to a DOM element. Re-renders only when read signals change.
const root = document.getElementById('root')!;
mount(root, () => (
<div>
<h1>Cart ({cart.state.value.items.length})</h1>
<ul>
{each(
cart.state.value.items,
(item) => (
<li>
{item.name}
<button data-action="remove" data-id={item.id}>×</button>
</li>
),
(item) => item.id,
)}
</ul>
<p>Doubled count: {doubled.value}</p>
</div>
));
// 5. Event delegation — one listener per event type, dispatched by data-action.
delegate(root, 'click', '[data-action="remove"]', (_e, btn) => {
cart.actions.remove((btn as HTMLElement).dataset.id!);
});For lists where most updates are pointwise (single-row edits, append-to-end, selection flips on individual rows), reach for arraySignal from the kerfjs/array-signal subpath. Mutators emit typed patches that each() applies in O(patches), not O(N):
import { arraySignal } from 'kerfjs/array-signal';
const rows = arraySignal<{ id: number; label: string }>([]);
mount(root, () => (
<ul>{each(rows, (r) => <li data-key={r.id}>{r.label}</li>)}</ul>
));
rows.push({ id: 1, label: 'a' }); // 1 insert patch
rows.update(0, (r) => ({ ...r, label: 'A' })); // 1 update patch
rows.move(0, 1); // 1 move patchThe class lives in its own subpath so apps that don't need it shed ~1 KB. Reads on rows.value are tracking, so computed(() => rows.value.filter(...)) works as expected. See docs/2-reactivity.md §2.6.
mount() wraps effect() so the render re-runs on signal changes. Sometimes you have a freshly-built template and an already-populated element and you just want to reconcile them once — no subscription, no re-render loop. That's morph:
import { morph, raw } from 'kerfjs';
morph(liveCard, freshlyBuiltCardEl); // Element template
morph(liveCard, '<article class="card">…</article>'); // raw HTML string
morph(liveCard, raw(htmlFromServer)); // SafeHtmlSame algorithm mount() uses internally — data-morph-skip, data-morph-skip-children, data-morph-preserve, focused-input value + selection preservation, the <details> / <dialog> user-agent-owned open rule all carry over. Use it for SSR-fragment hydration, page-refresh diffs, third-party widget remounts. See docs/4-render.md §4.4.3.
npm install kerfjsA companion ESLint plugin enforces kerf's hard rules at edit time. Four AST rules catch hard-rule violations — inline JSX event handlers, missing data-key in each(), nested mount(), and global JSX.IntrinsicElements augmentation. Two additional rules cover raw() XSS audit trails and AI-assistant config hygiene. The plugin is AST-only (no parser-services dependency), so it works with any TypeScript-ESLint setup.
npm install --save-dev eslint-plugin-kerfjs// eslint.config.js (flat config, ESLint v9+)
import kerfjs from 'eslint-plugin-kerfjs';
export default [kerfjs.configs.recommended];Full docs at brianwestphal.github.io/kerf/docs/eslint-plugin/ — legacy .eslintrc config, per-rule examples, and the rationale for which violations get lint rules vs. dev-warns vs. strict TS.
- Site: brianwestphal.github.io/kerf
- Docs:
docs/— overview · reactivity · stores · render · events · jsx · svg · API reference - Migrating: coming from another framework? — side-by-side TodoMVC translations + per-framework gotchas
- AI guide:
docs/ai/usage-guide.md— reference for AI tools fetching kerf docs (linked fromllms.txt) - ESLint plugin: brianwestphal.github.io/kerf/docs/eslint-plugin/ —
eslint-plugin-kerfjs; six rules (four hard-rule errors +no-raw-with-dynamic-argwarn +ai-assistant-configswarn) at edit time (source:eslint-plugin/) - Demo: live demo — eight sections exercising every primitive (counter, store-backed cart, focus survival, keyed list, morph-skip, SVG render, Tier-2 capture,
arraySignalpatches) - Repo: github.com/brianwestphal/kerf
A kerf is the narrow strip of material a saw blade removes when cutting — the smallest possible cut. The framework's job is the same: apply the smallest possible mutation to update your DOM.
(And yes, kerformance → performance jokes were written. They were also rejected.)
Pre-1.0 — API may evolve. See CHANGELOG.md for the current version and what's shipped.
If kerf saves you time on a project you ship, sponsoring on GitHub keeps it actively maintained. Any amount is appreciated.
MIT