
Security News
GitHub Actions Checkout Now Blocks Risky pull_request_target Checkouts
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.
Your AI app, live as you type — declare widgets, write event handlers, done. The same file runs in the browser or on a stateless Node server.
Your AI app, live as you type. Declare widgets, write on(event, handler) functions — done. The
same demo.tsx runs entirely in the browser (mount) or on a stateless Node server (serve); change
the last line to swap. The browser owns all state — the server keeps none, so there are no sessions.
knobkit.dev — 30-second tour + a live playground (nothing to install).
🛠️ Building with an AI agent? The knobkit-skills Agent Skill is the recommended way to scaffold and build a knobkit app fast — works in Claude Code or any Agent Skills–compatible agent.
import { knobkit, mic, output } from "knobkit";
import { pipeline } from "@huggingface/transformers";
const transcriber = await pipeline("automatic-speech-recognition", "onnx-community/whisper-base.en");
const recorder = mic();
const transcript = output();
const app = knobkit({ title: "Transcribe", widgets: [recorder, transcript] });
app.on(recorder.clip, async (samples) => {
const { text } = (await transcriber(samples)) as { text: string };
transcript.set(text.trim() || "(silence)");
});
app.serve(); // runs Whisper on Node — change to app.mount("#root") to run it in the browser via WebGPU
See examples/ — chatbots, image captioning,
live transcription, webcam filters; each a single demo.tsx.
npm create knobkit@latest my-app # prompts mount (browser) vs serve (node); or pass --mount / --serve
cd my-app && npm install && npm run dev
Already have a project? npm install knobkit. Requires Node ≥ 22.
knobkit dev # dev server — auto-detects the tier from mount()/serve() in the entry
knobkit build # build a mount app to static files in dist/
knobkit serve # run a serve app
knobkit playground # split-pane REPL: editor + live preview, file picker, edits round-trip to disk
Entry = your package.json "main" (override with knobkit dev other.tsx). --mount / --serve force
the tier; --port <n> sets the port (playground default 4317).
A handler is a plain on(event, async fn). Inside it you do exactly three things:
await box.value(), await convo.history() (a real
round-trip on serve);out.set(v), convo.say(m), log.push(line);returning an event from the handler (re-emitted, like a user action).setup(fn) runs once per session for async startup (load weights, fetch data). widget.busy(fn) wraps
a handler in a transient working span (a bar; drops the widget's input while running); disable() /
enable() is the persistent version. Widget methods only work inside a handler or setup.
mount("#root") | serve() | |
|---|---|---|
on(...) handlers | run in the browser | run on a stateless Node server |
| transport | local call | socket.io |
| use when | fits client-side (incl. WebGPU models) | needs the server (large models, secrets, native deps) |
mount builds to static files you can host anywhere; serve adds no session state. Widgets, handlers,
and methods are identical across both — only the last line changes.
Value inputs all share one shape: a changed event whose payload is the value, plus
await w.value() and w.set(v). (No .submitted/.uploaded — listen on changed, or use a
button's .clicked and read await input.value().)
| Factory (defaults) | changed value | Notes |
|---|---|---|
text({ placeholder?, lines? }) | string | lines = textarea rows (default 1) |
number({ value?, min?, max? }) | number | numeric stepper (init 0) |
slider({ value?, min?, max?, step? }) | number | min 0, max 100, step 1 |
dropdown({ choices, value? }) | string | choices: string[]; value defaults to choices[0] |
checkbox({ label?, value? }) | boolean | single toggle |
checkboxGroup({ choices, value? }) | string[] | multi-select |
radio({ choices, value? }) | string | single-select |
upload({ accept? }) | string | null | value is a data URL; accept default image/* |
Other inputs:
| Factory (defaults) | Events | Methods |
|---|---|---|
button({ label }) | clicked | set({ label }) |
mic({ every?, control?, hold? }) | clip (Float32Array), toggled | start(), stop(), await toggle(), await live(). every ms emits a clip every N ms (0 = hold/toggle only) |
webcam({ every?, control?, preview?, facing? }) | frame (data URL), toggled | same controls. every ms emits a frame every N ms (0 = preview only); facing "user"/"environment" |
chat({ placeholder?, voice?, images?, markdown? }) | sent ({ text, image? }), recorded | await history(), say(msg), append(token). markdown renders assistant replies; images/voice add attach/talk buttons |
Outputs (write-only; set(...) replaces the value):
| Factory (defaults) | Write / methods | Notes |
|---|---|---|
output({ format? }) | set(text) | format: "markdown" renders GFM |
json() | set(value) | pretty-printed JSON |
log() | push(line), await all() | append-only lines |
label() | set(string | { label?, confidences? }) | classifier result; confidences: { label, score }[] → bars |
html({ value? }) | set(markup) | raw HTML |
image() | set(urlOrDataUrl) | one image |
gallery() | set(items), add(item) | item: { src, caption? } |
audio({ autoplay? }) / video({ autoplay?, loop? }) | set(src) | URL or data URL |
progress({ label? }) | set(value, label?) | value is 0..1 |
file() | set({ name?, url } | url) | offer a download |
annotatedImage() | set(src, annotations?, colorMap?) | Annotation: { label, box?: [x0,y0,x1,y1], mask? } |
highlightedText() | set(spans, colorMap?) | span: { text, label? } (label omitted = plain) |
chart({ x, y, kind?, data?, maxHeight? }) | await data(), setData(rows), push(point) | x = category key; y = key or string[]; kind bar/line/area |
frame({ src?, doc?, sandbox?, title? }) | load(url), show(doc), clear() | iframe; event loaded |
Editable or read-only:
| Factory (defaults) | Events | Methods |
|---|---|---|
code({ value?, language?, editable?, wrap? }) | changed (string) | await value(), set(src), setLanguage(lang). editable: false = viewer; wrap soft-wraps |
table({ columns?, rows?, editable?, maxHeight? }) | edited ({ row, key, value }) | await data(), setRows, setColumns, addRow, setCell. Column: { key, label?, type?, width? } |
Custom: widget({ state, view, fold?, behavior? }) builds a widget from scratch — state is its
data, view(state, emit) renders it, fold applies events to state.
widgets is a tree of widget objects (no keys/strings). An array is an implicit col:
knobkit({ widgets: col(photo, row(size, go), caption) });
grid([a, b, c, d], { cols: 2 });
tabs([{ label: "One", content: a }, { label: "Two", content: b }]);
accordion({ label: "Advanced", open: false }, x, y);
Containers are widgets whose state is their arrangement, so a handler can restructure the UI at
runtime — panel.add(chart), await panel.remove(sidebar).
Set on knobkit({ … }), or flip at runtime with setTheme / setDensity:
theme — "system" (default) | "light" | "dark".density — "xs" | "sm" | "md" | "lg" | "xl" (default md) — spacing, control sizes, radii, type.fill: true — full-bleed shell that fills the viewport (for split panes / dashboards) instead of
the centered card.Everything renders from CSS custom properties (--pu-bg, --pu-accent, --pu-gap, the --pu-series-*
chart palette, …); theme/density just remap them, so one switch restyles the whole kit (including the
code editor, table, and chart). The attributes inherit, so you can scope them to one container; to
rebrand, override the tokens in your CSS (e.g. :root { --pu-accent: rebeccapurple }).
pnpm install
pnpm -F knobkit build # library + browser client bundle
pnpm -F knobkit test # vitest
pnpm typecheck # all packages
See CLAUDE.md for the architecture and how to add a widget.
FAQs
Your AI app, live as you type — declare widgets, write event handlers, done. The same file runs in the browser or on a stateless Node server.
The npm package knobkit receives a total of 15 weekly downloads. As such, knobkit popularity was classified as not popular.
We found that knobkit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.

Product
Socket MCP now lets AI assistants review org alerts, investigate threats using the Socket threat feed, and inspect package files in addition to dependency scoring.