svelte by vercel-labs/json-render
npx skills add https://github.com/vercel-labs/json-render --skill svelte将 json-render 规范转换为 Svelte 组件树的 Svelte 5 渲染器。
<script lang="ts">
import { Renderer, JsonUIProvider } from "@json-render/svelte";
import type { Spec } from "@json-render/svelte";
import Card from "./components/Card.svelte";
import Button from "./components/Button.svelte";
interface Props {
spec: Spec | null;
}
let { spec }: Props = $props();
const registry = { Card, Button };
</script>
<JsonUIProvider>
<Renderer {spec} {registry} />
</JsonUIProvider>
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/svelte";
import { z } from "zod";
export const catalog = defineCatalog(schema, {
components: {
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary"]).nullable(),
}),
description: "可点击的按钮",
},
Card: {
props: z.object({ title: z.string() }),
description: "带标题的卡片容器",
},
},
});
组件应接受 BaseComponentProps<TProps>:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
interface BaseComponentProps<TProps> {
props: TProps; // 此组件的已解析属性
children?: Snippet; // 子元素(使用 {@render children()})
emit: (event: string) => void; // 触发命名事件
bindings?: Record<string, string>; // 属性名到状态路径的映射(用于 $bindState)
loading?: boolean; // 规范流式传输时为 true
}
<!-- Button.svelte -->
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{ label: string; variant?: string }> {}
let { props, emit }: Props = $props();
</script>
<button class={props.variant} onclick={() => emit("press")}>
{props.label}
</button>
<!-- Card.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{ title: string }> {
children?: Snippet;
}
let { props, children }: Props = $props();
</script>
<div class="card">
<h2>{props.title}</h2>
{#if children}
{@render children()}
{/if}
</div>
import { defineRegistry } from "@json-render/svelte";
import { catalog } from "./catalog";
import Card from "./components/Card.svelte";
import Button from "./components/Button.svelte";
const { registry, handlers, executeAction } = defineRegistry(catalog, {
components: {
Card,
Button,
},
actions: {
submit: async (params, setState, state) => {
// 处理操作
},
},
});
Svelte 模式使用元素树格式:
{
"root": "card1",
"elements": {
"card1": {
"type": "Card",
"props": { "title": "Hello" },
"children": ["btn1"]
},
"btn1": {
"type": "Button",
"props": { "label": "Click me" }
}
}
}
在元素上使用 visible 来根据状态显示/隐藏:
{ "$state": "/path" } - 真值检查{ "$state": "/path", "eq": value } - 相等性检查{ "$state": "/path", "not": true } - 假值检查{ "$and": [cond1, cond2] } - AND 条件{ "$or": [cond1, cond2] } - OR 条件JsonUIProvider 组合了所有上下文。单独的上下文:
| 上下文 | 用途 |
|---|---|
StateContext | 在组件间共享状态(JSON 指针路径) |
ActionContext | 处理通过事件系统分发的操作 |
VisibilityContext | 启用基于状态的条件渲染 |
ValidationContext | 表单字段验证 |
组件使用 emit 来触发命名事件。元素的 on 字段将事件映射到操作绑定:
<!-- Button.svelte -->
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{ label: string }> {}
let { props, emit }: Props = $props();
</script>
<button onclick={() => emit("press")}>{props.label}</button>
{
"type": "Button",
"props": { "label": "Submit" },
"on": { "press": { "action": "submit" } }
}
setState 操作会自动处理并更新状态模型:
{
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
}
其他内置操作:pushState、removeState、push、pop。
表达式形式在组件接收属性之前解析:
{"$state": "/state/key"} - 从状态读取{"$bindState": "/form/email"} - 读取 + 写回状态{"$bindItem": "field"} - 读取 + 写回重复项{"$cond": <condition>, "$then": <value>, "$else": <value>} - 条件值对于组件内部的可写绑定,使用 getBoundProp:
<script lang="ts">
import { getBoundProp } from "@json-render/svelte";
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{ value?: string }> {}
let { props, bindings }: Props = $props();
let value = getBoundProp<string>(
() => props.value,
() => bindings?.value,
);
</script>
<input bind:value={value.current} />
推荐的辅助函数:
getStateValue(path) - 返回 { current }(可读写)getBoundProp(() => value, () => bindingPath) - 返回 { current }(绑定时可读写)isVisible(condition) - 返回 { current }(布尔值)getAction(name) - 返回 { current }(注册的处理程序)高级上下文访问:
getStateContext()getActionContext()getVisibilityContext()getValidationContext()getOptionalValidationContext()getFieldValidation(ctx, path, config?)使用 createUIStream 进行规范流式传输:
<script lang="ts">
import { createUIStream, Renderer } from "@json-render/svelte";
const stream = createUIStream({
api: "/api/generate-ui",
onComplete: (spec) => console.log("完成", spec),
});
async function generate() {
await stream.send("创建登录表单");
}
</script>
<button onclick={generate} disabled={stream.isStreaming}>
{stream.isStreaming ? "生成中..." : "生成 UI"}
</button>
{#if stream.spec}
<Renderer spec={stream.spec} {registry} loading={stream.isStreaming} />
{/if}
使用 createChatUI 进行聊天 + UI 响应:
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("构建一个设置面板");
每周安装量
148
代码仓库
GitHub 星标数
13.3K
首次出现
2026年3月7日
安全审计
安装于
codex145
cursor144
opencode143
gemini-cli142
kimi-cli142
github-copilot142
Svelte 5 renderer that converts json-render specs into Svelte component trees.
<script lang="ts">
import { Renderer, JsonUIProvider } from "@json-render/svelte";
import type { Spec } from "@json-render/svelte";
import Card from "./components/Card.svelte";
import Button from "./components/Button.svelte";
interface Props {
spec: Spec | null;
}
let { spec }: Props = $props();
const registry = { Card, Button };
</script>
<JsonUIProvider>
<Renderer {spec} {registry} />
</JsonUIProvider>
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/svelte";
import { z } from "zod";
export const catalog = defineCatalog(schema, {
components: {
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary"]).nullable(),
}),
description: "Clickable button",
},
Card: {
props: z.object({ title: z.string() }),
description: "Card container with title",
},
},
});
Components should accept BaseComponentProps<TProps>:
interface BaseComponentProps<TProps> {
props: TProps; // Resolved props for this component
children?: Snippet; // Child elements (use {@render children()})
emit: (event: string) => void; // Fire a named event
bindings?: Record<string, string>; // Map of prop names to state paths (for $bindState)
loading?: boolean; // True while spec is streaming
}
<!-- Button.svelte -->
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{ label: string; variant?: string }> {}
let { props, emit }: Props = $props();
</script>
<button class={props.variant} onclick={() => emit("press")}>
{props.label}
</button>
<!-- Card.svelte -->
<script lang="ts">
import type { Snippet } from "svelte";
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{ title: string }> {
children?: Snippet;
}
let { props, children }: Props = $props();
</script>
<div class="card">
<h2>{props.title}</h2>
{#if children}
{@render children()}
{/if}
</div>
import { defineRegistry } from "@json-render/svelte";
import { catalog } from "./catalog";
import Card from "./components/Card.svelte";
import Button from "./components/Button.svelte";
const { registry, handlers, executeAction } = defineRegistry(catalog, {
components: {
Card,
Button,
},
actions: {
submit: async (params, setState, state) => {
// handle action
},
},
});
The Svelte schema uses the element tree format:
{
"root": "card1",
"elements": {
"card1": {
"type": "Card",
"props": { "title": "Hello" },
"children": ["btn1"]
},
"btn1": {
"type": "Button",
"props": { "label": "Click me" }
}
}
}
Use visible on elements to show/hide based on state:
{ "$state": "/path" } - truthy check{ "$state": "/path", "eq": value } - equality check{ "$state": "/path", "not": true } - falsy check{ "$and": [cond1, cond2] } - AND conditions{ "$or": [cond1, cond2] } - OR conditionsJsonUIProvider composes all contexts. Individual contexts:
| Context | Purpose |
|---|---|
StateContext | Share state across components (JSON Pointer paths) |
ActionContext | Handle actions dispatched via the event system |
VisibilityContext | Enable conditional rendering based on state |
ValidationContext | Form field validation |
Components use emit to fire named events. The element's on field maps events to action bindings:
<!-- Button.svelte -->
<script lang="ts">
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{ label: string }> {}
let { props, emit }: Props = $props();
</script>
<button onclick={() => emit("press")}>{props.label}</button>
{
"type": "Button",
"props": { "label": "Submit" },
"on": { "press": { "action": "submit" } }
}
The setState action is handled automatically and updates the state model:
{
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
}
Other built-in actions: pushState, removeState, push, pop.
Expression forms resolved before your component receives props:
{"$state": "/state/key"} - read from state{"$bindState": "/form/email"} - read + write-back to state{"$bindItem": "field"} - read + write-back for repeat items{"$cond": <condition>, "$then": <value>, "$else": <value>} - conditional valueFor writable bindings inside components, use getBoundProp:
<script lang="ts">
import { getBoundProp } from "@json-render/svelte";
import type { BaseComponentProps } from "@json-render/svelte";
interface Props extends BaseComponentProps<{ value?: string }> {}
let { props, bindings }: Props = $props();
let value = getBoundProp<string>(
() => props.value,
() => bindings?.value,
);
</script>
<input bind:value={value.current} />
Preferred helpers:
getStateValue(path) - returns { current } (read/write)getBoundProp(() => value, () => bindingPath) - returns { current } (read/write when bound)isVisible(condition) - returns { current } (boolean)getAction(name) - returns { current } (registered handler)Advanced context access:
getStateContext()getActionContext()getVisibilityContext()getValidationContext()getOptionalValidationContext()getFieldValidation(ctx, path, config?)Use createUIStream for spec streaming:
<script lang="ts">
import { createUIStream, Renderer } from "@json-render/svelte";
const stream = createUIStream({
api: "/api/generate-ui",
onComplete: (spec) => console.log("Done", spec),
});
async function generate() {
await stream.send("Create a login form");
}
</script>
<button onclick={generate} disabled={stream.isStreaming}>
{stream.isStreaming ? "Generating..." : "Generate UI"}
</button>
{#if stream.spec}
<Renderer spec={stream.spec} {registry} loading={stream.isStreaming} />
{/if}
Use createChatUI for chat + UI responses:
const chat = createChatUI({ api: "/api/chat-ui" });
await chat.send("Build a settings panel");
Weekly Installs
148
Repository
GitHub Stars
13.3K
First Seen
Mar 7, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex145
cursor144
opencode143
gemini-cli142
kimi-cli142
github-copilot142
Angular Signal Forms 实验性表单 API:基于 Signal 的模型驱动表单架构详解
449 周安装