ui-sdk 的组件都建在一套行为之上:焦点、键盘导航、选择、弹出层、拖放,以及它们所需的 ARIA。当某个组件 ui-sdk 没有提供(步进器、分隔条、穿梭框、emoji 选择器),你用 @ui-sdk/core 的同一套行为自己组合,得到的组件自动拥有相同的键盘、焦点、服务端渲染与从右到左支持。
这一页讲清楚原语(primitive)是什么、怎么定义、怎么渲染进你的框架。完整参考随包附在 node_modules/@ui-sdk/<框架>/docs/core.md(按标题检索,它很大)。
它是什么
一个原语描述组件的行为:它的状态、改变状态的函数、使用状态的部件。它本身不渲染任何东西。行为留在框架无关的 core 里,适配器把它接到 React、Vue、Svelte、Solid 或 Web Components。换框架只换渲染方式,行为不变。
定义一个原语
以静音按钮为例:一个状态 muted,一个部件 root。
import { definePrimitive } from "@ui-sdk/core";
const muteButton = definePrimitive({
name: "mute-button",
state: () => ({ muted: false }),
api: (context) => ({
toggle: () => context.store.set({ muted: !context.store.get().muted }),
}),
slots: (slot) => ({
root: slot({
attrs: (state) => ({
type: "button",
"aria-pressed": String(state.muted),
}),
press: ({ api }) => api.toggle(),
}),
}),
});
export { muteButton };state把muted初始化为false。api定义toggle,在true与false间切换。slots描述部件。唯一的 slotroot渲染时变成按钮。attrs把状态变成 HTML 属性:muted为true时根得到aria-pressed="true"。press在根被激活时调用toggle。
这就是全部行为。name 会给每个部件的 data-slot 加前缀(如 tabs 的 list slot 得到 tabs-list),root slot 只用 name 本身(data-slot="mute-button")。状态属性可用 core 的助手:openAttrs(open) 返回 data-open/data-closed,checkedAttrs(checked) 返回 data-checked/data-unchecked/data-indeterminate。
选项
给原语传入选项时,用类型描述它,在 options 里列出每项的「种类」,并用该类型标注 state 的参数:
type CopyButtonOptions = { text?: string; onCopy?: (text: string) => void };
options: { text: "string", onCopy: "callback" },
state: (options: CopyButtonOptions) => ({ copied: false }),种类告诉适配器如何处理每个值:web components 从属性读 text、把 onCopy 变成事件;Vue 把 callback 当作 emit。常用种类有 "string"/"number"/"boolean"/"list"/"value"/"callback"/"action"/"id"/"key"。在 api 里用 context.options() 读当前值;api 每实例只运行一次,所以在方法内部读选项可避免持有旧值。选项变化需要额外处理时用 context.onOptions。
渲染进你的框架
适配器把原语接到框架。先装对应包(@ui-sdk/react、@ui-sdk/vue、@ui-sdk/svelte、@ui-sdk/solid、@ui-sdk/web),再用 createParts 从原语创建部件。React:
"use client";
import { createParts } from "@ui-sdk/react/primitive";
import { muteButton } from "@/lib/mute-button";
const { Root: MuteButton } = createParts(muteButton, { root: "button" });
export { MuteButton };{ root: "button" } 表示根部件渲染 <button>。然后在页面里用:
import { MuteButton } from "@/components/ui/mute-button";
<MuteButton>静音</MuteButton>;Vue、Svelte、Solid 用各自的 createParts(@ui-sdk/vue/primitive 等),写法相同。Web Components 注册自定义元素:
import { createParts, wrappedButton } from "@ui-sdk/web";
import { muteButton } from "@/lib/mute-button";
createParts(muteButton, { root: { tag: "mute-button", target: wrappedButton } });<mute-button><button>静音</button></mute-button>因为用了原生 <button>,点击、Enter、Space 都到达原语的 press,屏幕阅读器从 aria-pressed 读到按下态。详见框架适配。
组合行为
更复杂的组件由多个共享状态的行为组成。以一个评分组件为例:先用共享状态建立一组星,每颗星是一个带 value 的部件,组负责单一 tab 焦点(roving tabindex)与方向键移动。core 提供这些行为积木,你把它们拼成原语,而不是从零写键盘逻辑。完整 walkthrough 见 core 文档的「Compose Behaviors」。
让你的组件可被智能体使用
agent 键告诉智能体桥哪些 api 方法可被调用:
agent: { actions: ["setValue", "clear"], closed: true },放在 slots 之后,让 TypeScript 先读到 api。没有它时,桥自动选取名字像变更的方法(如 setValue、toggle),事件处理器(如 keydown)保持内部。快照读取原语声明的 controlled 状态键;未声明时读取存在的 open、value、checked。详见连接智能体。
样式与测试
你的组件和 ui-sdk 自带组件一样无样式:用 data-slot 与状态属性写 CSS,把值作为 CSS 变量传入。参见样式与动画。
core 附带测试工具,帮你在成品组件里检查键盘行为、焦点与 ARIA。每个自定义组件都应至少有一个智能体测试(用 createAgent 读快照、调用动作)。参见智能体概述。