- name
- json-render-catalog
- compatibility
- Claude Code 2.1.277+
- description
- json-render component catalog patterns for AI-safe generative UI. Define Zod-typed catalogs that constrain what AI can generate, use @json-render/shadcn for 36 pre-built components, optimize specs with YAML mode, and apply the three edit modes (patch/merge/diff) for progressive updates. Use when building AI-generated UIs, defining component catalogs, or integrating json-render into React/Vue/Svelte/React Native/Ink/Next.js projects.
- user-invocable
- false
- disable-model-invocation
- false
- context
- inherit
- metadata
- {"category":"frontend","upstream-package":"@json-render/core","upstream-version-tested":"0.21.0","shadcn-component-count":36,"version":"1.3.0","author":"OrchestKit","complexity":"medium","tags":"json-render, genui, zod, catalog, shadcn, ai-ui, component-catalog, vercel"}
Upstream version reference: `@json-render/core` 0.21.0 (2026-09-30).
# json-render Component Catalogs
json-render (Vercel Labs, 12.9K stars, Apache-2.0) is a framework for AI-safe generative UI. AI generates flat-tree JSON (or YAML) specs constrained to a developer-defined catalog — the catalog is the contract between your design system and AI output. If a component or prop is not in the catalog, AI cannot generate it.
## Storybook → catalog import (#1529, 2026-04)
When the project ships a Storybook setup, **import the catalog from Storybook stories** instead of hand-writing one. The bundled importer at `scripts/storybook-to-catalog.mjs` reads a `@storybook/addon-mcp` `list-all-documentation` manifest and emits a Zod-typed `catalog.ts` plus a `components.tsx` registry.
```bash
node "${CLAUDE_SKILL_DIR}/scripts/storybook-to-catalog.mjs" storybook-manifest.json \
--out src/genui/catalog.ts \
--components src/genui/components.tsx \
--project-root .
```
Storybook becomes the single source of truth — adding a story automatically expands the AI-allowed surface; removing one shrinks it. AI safety is enforced at import: callbacks, raw object props, and `z.any()` are dropped. Full mapping: `references/storybook-import.md`. Companion fixture for testing: `references/storybook-fixture.json`.
## New in 2026-04 → 2026-08 (json-render 0.14 → 0.20)
- **Named slots (0.20)** — `UIElement` gains `slots?: Record<string, string[]>`, a catalog declares which it accepts via `slots: ["default", "header", "footer"]`, and a React registry component receives named content as `slots?.header` while `children` stays the default slot. Prefer this over encoding layout regions as separate sibling elements.
- **Nested repeats and item-scoped visibility (0.20)** — `repeat.statePath` accepts an item-relative `{"$item": "employees"}` form (new `RepeatStatePath` type plus `resolveRepeatStatePath` / `resolveRepeatItemStatePath`), and a `repeat` combined with `visible: {"$item": "status", "eq": "todo"}` on the same element now filters items instead of erroring. `validateSpec` gains `invalid_visible`, `repeat_without_children`, `repeat_item_outside_scope` and `repeat_state_mismatch` issue codes.
- **Custom directives API (0.19)** — `@json-render/core` now ships `defineDirective`, letting you declare new JSON shapes (e.g. `$format`, `$math`) that resolve to computed values at render time. Directives compose by nesting and resolve inside-out. All four renderers (React, Vue, Svelte, Solid) have built-in directive resolution. This is the safe escape hatch for computed values without widening the catalog to `z.any()`.
- **`@json-render/directives` package (0.19)** — seven ready-made directives: `$format` (date / currency / number / percent via `Intl`), `$math` (add, subtract, multiply, divide, mod, min, max, round, floor, ceil, abs), `$concat`, `$count`, `$truncate`, `$pluralize`, `$join`. Plus `createI18nDirective` for `$t` translation keys with `{{param}}` interpolation, and `standardDirectives` for one-line registration. Register once, use in any spec — AI no longer needs string-mangling or duplicated literals.
- **Devtools ecosystem (0.18)** — five new packages: `@json-render/devtools` core + framework adapters for React, Vue, Svelte, Solid. Inspector panel has six tabs (Spec, State, Actions, Stream, Catalog, Pick) with DOM element picking that maps back to spec keys. Tree-shakes to `null` in production. Companion Next.js demo app shipped with AI-chat + catalog integration. Action observer infrastructure exposed for adapters to mirror events into the panel.
- **Zod 4 fix (0.18)** — `formatZodType` now correctly handles `z.record()`, `z.default()`, and `z.literal()` (previously produced empty/wrong prompt output).
- **Three edit modes (0.14)** — `patch` (RFC 6902), `merge` (RFC 7396), `diff` (unified) for progressive AI refinements. `buildEditUserPrompt()` + `diffToPatches()` + `deepMergeSpec()` in `@json-render/core`.
- **`@json-render/yaml` (0.14)** — official YAML wire format + streaming parser; `buildUserPrompt({ format: 'yaml' })`.
- **`@json-render/ink` (0.15)** — render catalogs to terminal UIs (Ink-based, 20+ components) using the same spec.
- **`@json-render/next` (0.16)** — generate full Next.js apps (routes, layouts, SSR, metadata) from a single spec.
- **`@json-render/shadcn-svelte` (0.16)** — 36-component Svelte 5 + Tailwind mirror of the React shadcn catalog.
- **shadcn catalog at 36 components** (was documented as 29 — the count was wrong even at 0.13). Use `@json-render/shadcn` as-is, or spread `shadcnComponentDefinitions` together with your own definitions.
- **`@json-render/react-three-fiber`** ships 19 components (verified 2026-07-31 against the upstream skill; do not restate the roster here, see Upstream coverage).
- **`@json-render/mcp`** — upgrade plain MCP tool JSON into interactive iframes inside Claude/Cursor/ChatGPT conversations. See the `ork:mcp-visual-output` skill.
- **MCP multi-surface**: same spec renders to React, PDF (`@json-render/react-pdf`), email (`@json-render/react-email`), terminal (Ink), Next.js apps, and Remotion videos.
## Directives — @json-render/directives (0.19)
Directives are the safe escape hatch for computed values. AI emits a `$`-prefixed object, the renderer resolves it inside-out before the component receives props — the catalog stays strict (no `z.any()` widening) and the spec stays declarative. The `@json-render/directives` package ships seven prebuilt directives plus an i18n factory; `standardDirectives` exports them as one array for one-line registration. Directives nest freely (e.g. `$format` wrapping `$math`) and are resolved by all four renderer integrations (React, Vue, Svelte, Solid).
### Registration
```tsx
import { defineRegistry, JSONUIProvider, Renderer } from '@json-render/react'
import { standardDirectives, createI18nDirective } from '@json-render/directives'
const directives = [
...standardDirectives,
createI18nDirective({
locale: 'en',
fallbackLocale: 'en',
messages: { en: { greeting: 'Hello, {{name}}!' } },
}),
]
const { registry } = defineRegistry(catalog, { components })
// directives register on the provider (RendererProps has no directives prop)
<JSONUIProvider registry={registry} directives={directives}>
<Renderer spec={spec} registry={registry} />
</JSONUIProvider>
```
### The seven prebuilt directives
| Directive | Purpose | Minimal usage |
|-----------|---------|---------------|
| `$format` | `Intl`-based formatting for `date`, `currency`, `number`, `percent`. Supports `locale`, `currency`, `notation`, and `style: "relative"` for human-readable date deltas. | `{ "$format": "currency", "value": 1299, "currency": "USD" }` → `$1,299.00` |
| `$math` | Arithmetic — `add`, `subtract`, `multiply`, `divide`, `mod`, `min`, `max`, `round`, `floor`, `ceil`, `abs`. Division by zero returns `0`; non-numeric inputs coerce to `0`. | `{ "$math": "multiply", "a": { "$state": "/qty" }, "b": 9.99 }` |
| `$concat` | Joins an array of dynamic values into a string, resolving each element through the directive pipeline first. | `{ "$concat": ["Hello, ", { "$state": "/user/name" }, "!"] }` |
| `$count` | Length of an array or string; `0` for anything else. | `{ "$count": { "$state": "/items" } }` |
| `$truncate` | Truncate to `length` (default 100) with optional `suffix` (default `...`). No-op if already short enough. | `{ "$truncate": { "$state": "/bio" }, "length": 80 }` |
| `$pluralize` | Singular/plural/zero selection. Prepends the count automatically (`"3 items"`, `"1 item"`, or the literal `zero` form). | `{ "$pluralize": { "$state": "/cart/count" }, "zero": "no items", "one": "item", "other": "items" }` |
| `$join` | Join an array with a `separator` (default `", "`). | `{ "$join": { "$state": "/tags" }, "separator": " · " }` |
`createI18nDirective({ locale, messages, fallbackLocale? })` registers a `$t` directive with `{{param}}` interpolation: `{ "$t": "greeting", "params": { "name": "Ada" } }`.
### Custom directives via `defineDirective`
`defineDirective` lives in `@json-render/core` (0.19+). A directive declares a Zod schema for its JSON shape and a `resolve(raw, ctx)` function — use `resolvePropValue(raw.field, ctx)` to recursively resolve any nested directive or state reference before computing.
```ts
import { defineDirective, resolvePropValue } from '@json-render/core'
import { z } from 'zod'
export const initialsDirective = defineDirective({
name: '$initials',
description: 'First letter of each word, uppercased.',
schema: z.object({ $initials: z.unknown() }),
resolve(raw, ctx) {
const text = String(resolvePropValue(raw.$initials, ctx) ?? '')
return text.split(/\s+/).map((w) => w[0]?.toUpperCase() ?? '').join('')
},
})
```
Spread into the renderer alongside `standardDirectives`: `directives={[...standardDirectives, initialsDirective]}`. Keep the schema tight — directives are the only place where AI gets to emit non-catalog JSON, so let Zod enforce shape just like a component prop.
## Upstream coverage (do not restate)
json-render ships its own per-package skills. This skill wraps them and keeps only the
delta: our Storybook import path, our catalog constraints, and the scars in
`references/ork-delta.md`. Do not copy vendor rosters or API tables back in.
| Topic | First-party source |
|-------|--------------------|
| Core API (`defineSchema`, `defineCatalog`, prompts, spec streaming, validation, `StateStore`) | https://github.com/vercel-labs/json-render `skills/core/SKILL.md` |
| Spec format, dynamic prop expressions (`$state` / `$bindState` / `$cond` / `$template` / `$computed`), `watch`, visibility | https://github.com/vercel-labs/json-render `skills/core/SKILL.md` and `skills/react/SKILL.md` |
| Built-in actions (`setState`, `pushState`, `removeState`, `validateForm`) and the event system | https://github.com/vercel-labs/json-render `skills/react/SKILL.md` |
| shadcn component roster and prop schemas (36 components, React and Svelte) | `vercel:shadcn` skill, plus https://github.com/vercel-labs/json-render `skills/shadcn/SKILL.md` and `skills/shadcn-svelte/SKILL.md` |
| Per-renderer components and APIs (Vue, Svelte, Solid, React Native, Ink, Next.js, PDF, email, image, Remotion, react-three-fiber) | https://github.com/vercel-labs/json-render `skills/<package>/SKILL.md` |
| YAML wire format, fences, streaming compiler, edit modes | https://github.com/vercel-labs/json-render `skills/yaml/SKILL.md` |
| State adapters (`zustandStateStore`, `reduxStateStore`, `jotaiStateStore`, `xstateStoreStateStore`) | https://github.com/vercel-labs/json-render `skills/zustand/SKILL.md`, `skills/redux/SKILL.md`, `skills/jotai/SKILL.md`, `skills/xstate/SKILL.md` |
| MCP Apps integration (`createMcpApp`, iframe client) | `ork:mcp-visual-output`, plus https://github.com/vercel-labs/json-render `skills/mcp/SKILL.md` |
| Migrating a hand-rolled JSON-to-component mapper | https://github.com/vercel-labs/json-render `skills/core/SKILL.md` (catalog + spec contract is the target shape) |
Our delta, the part no upstream doc carries: `references/ork-delta.md`.
## Quick Reference
| Category | Rules | Impact | When to Use |
|----------|-------|--------|-------------|
| [Catalog Definition](#catalog-definition) | 1 | HIGH | Defining component catalogs with Zod |
| [Prop Constraints](#prop-constraints) | 1 | HIGH | Constraining AI-generated props for safety |
| [Token Optimization](#token-optimization) | 1 | MEDIUM | Reducing token usage with YAML mode |
**Total: 3 rules across 3 categories**
## How json-render Works
1. **Developer defines a catalog** — Zod-typed component definitions with constrained props
2. **AI generates a spec** — flat-tree JSON/YAML referencing only catalog components
3. **Runtime renders the spec** — `<Renderer>` component validates and renders each element
The catalog is the safety boundary. AI can only reference types that exist in the catalog, and props are validated against Zod schemas at runtime. This prevents hallucinated components and invalid props from reaching the UI.
## Quick Start — 3 Steps
### Step 1: Define a Catalog
```typescript
import { defineCatalog } from '@json-render/core'
View on GitHub