Notto UI is 14 intent-level primitives and a runtime renderer — shipped as @nottohq/ui. Agents emit JSON; the renderer validates against zod and mounts a safe React tree. No geometry values, no CSS escape hatches — a single uniform vocabulary humans and LLMs both speak.
npm install @nottohq/uiEvery primitive speaks the same intent-level prop DSL — tone · variant · size · gap · padding · align · justify. No geometry values. No style escape hatch. Agents and humans compose the same trees.
Serializable, stateless, safe to emit as agent JSON. Validated against zod schemas before mount.
Top-level document shell. Theme preset, width, padding.
One-axis flow. Direction, gap, align, justify. The spine of every composition.
Rectangular surface with tone + padding. Used as a container, never for layout math.
All prose. Variant sets role; tone sets foreground meaning.
Named glyph from the allowlist. Unknown names fail at validation.
Primary action. Emits a typed action reference — never an inline handler.
Safe href allowlist. Tabnabbing defense forced on target=_blank.
Compact status marker. Tone carries semantic meaning, not colour values.
Preformatted monospace block. Text is escaped before mount — no HTML injection.
Outside the renderer surface — used directly in React. Too stateful or too open to serialise.
Label + control + help + error. Controlled input; not serialisable.
Container with header / body / footer render slots. Functions as children.
Rows resolved by a render fn. Sort + select state lives in React, not JSON.
Portal + focus trap + escape. Open state is React-owned, never agent-emitted.
Queued notifications via ToastProvider + useToast. Imperative surface — not declarable.
Edit the document on the left. The renderer validates against zod on every change — invalid trees never reach React. Try a javascript: href, an unknown primitive, a sixteen-level nesting.
Agents produce untrusted input. Everything crossing the boundary is validated, clamped, or rejected. Errors never echo pathological input back to the caller.
https, mailto, tel, and internal refs. Rejects javascript:, data:, protocol-relative, and user:pass@ phishing forms.target="_blank" link auto-gets rel="noopener noreferrer". Cannot be overridden through the DSL.1000), depth (20), and leaf text (10 kB). A runaway agent can't hang the renderer.SKILL.md is a markdown file the library ships with. Drop it in Claude, Cursor, Cline, or Copilot; the model learns the primitives and emits valid trees on the first try.
--- name: nottohq-ui version: 0.1.1 purpose: emit valid @nottohq/ui JSON trees --- # Vocabulary Primitives: Page · Stack · Box · Text · Icon · Button · Link · Badge · CodeBlock Every primitive accepts a subset of: tone · neutral | primary | success | danger | … variant · solid | soft | outline | ghost size · sm | md | lg gap · 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 padding · same scale as gap # Rules - Never pass style. Never pass arbitrary CSS. - Root of a renderer tree is always Page or Stack. - Layout with Stack, never with Box. - Links need a scheme (https:, mailto:, tel:).
{ "type": "Stack", "props": { "gap": 4, "padding": 5 }, "children": [ { "type": "Badge", "props": { "tone": "success" }, "children": "CONFIRMED" }, { "type": "Text", "props": { "variant": "title" }, "children": "Booking confirmed" }, { "type": "Button", "props": { "action": "open" }, "children": "Open" } ] }▍
The DSL talks about tone, variant, gap — roles, not pixels. Pixel decisions live in the theme, once. Agents can't invent a 17px padding.
React is the universal runtime. No custom VM, no per-provider coupling, no proprietary format. If an LLM emits valid JSON, the renderer mounts it.
Markdown is the universal onboarding. SKILL.md works with Claude, Cursor, Cline, Copilot — anything that reads a file.