---
title: "内置组件 — zenit Zig UI 文档"
description: "理解一个 zenit 组件承诺了什么、何时下降抽象层，以及挂载、浮层和测试三份契约。逐个组件的录像、props 与结果类型在组件站。"
url: https://zenit.z.express/zh/docs/guide/components
language: zh-CN
alternate_en: https://zenit.z.express/docs/guide/components.md
alternate_es: https://zenit.z.express/es/docs/guide/components.md
alternate_ja: https://zenit.z.express/ja/docs/guide/components.md
alternate_ko: https://zenit.z.express/ko/docs/guide/components.md
alternate_fr: https://zenit.z.express/fr/docs/guide/components.md
alternate_de: https://zenit.z.express/de/docs/guide/components.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 内置组件

理解一个 zenit 组件承诺了什么、何时下降抽象层，以及挂载、浮层和测试三份契约。逐个组件的录像、props 与结果类型在[组件站](https://zenit.z.express/zh/components)。

## 这里的「组件」包含什么

zenit 组件不是把颜色和圆角拼起来的 helper。一个公开组件通常同时封装节点结构、持久状态、事件路由、焦点与键盘语义、无障碍属性、主题 token、浮层生命周期，以及可以被 Harness 定位和回读的测试边界。它们都从 `ui.widgets` 导出。

**51**组件站收录的 Story

**6**使用场景分类（另有 Labs）

**1:1**Story 与 E2E test\_id

ANATOMY

mount 内部先创建子 Scope；它持有的一切会随父 Scope 一起释放。

## 先选择正确的抽象层

| 需求 | 使用层 | 你仍然负责 |
| --- | --- | --- |
| 标准产品控件 | `ui.widgets.*` | 业务状态、文案、回调与布局位置 |
| 自定义视觉，保留成熟交互 | `ui.hooks` · `ui.interaction` · `ui.select_headless` | 绘制、token、a11y label 与组合边界 |
| 全新交互模式 | `ui.Node` + `ui.events` | 命中、键盘、焦点、IME、a11y 和测试契约 |

> TIP
> 
> **从 widgets 开始。** 只有现有组件的行为模型不适合时才下降抽象层。换一种视觉通常不值得重新实现焦点、输入法、浮层 dismiss 和辅助功能。

## 组件目录

每个组件在[组件站](https://zenit.z.express/zh/components)都有独立页面：真实 `zenit Storybook.app` 的录像、从源码生成的 props 与结果类型，以及录像正在证明的行为。录像由 Harness 按 `test_id` 定位、驱动虚拟鼠标或输入，并直接录制 Metal drawable。

| 分类 | 数量 | 代表组件 |
| --- | --- | --- |
| 操作与选择 | 7 | [Button](https://zenit.z.express/zh/components/button) · [Checkbox](https://zenit.z.express/zh/components/checkbox) · [Switch](https://zenit.z.express/zh/components/switch) · [RadioGroup](https://zenit.z.express/zh/components/radio) · [Slider](https://zenit.z.express/zh/components/slider) … |
| 输入与表单 | 10 | [Input](https://zenit.z.express/zh/components/input) · [Textarea](https://zenit.z.express/zh/components/textarea) · [Select](https://zenit.z.express/zh/components/select) · [Input · Select · Button](https://zenit.z.express/zh/components/formcompose) · [ComboBox](https://zenit.z.express/zh/components/combobox) … |
| 数据展示 | 17 | [Badge](https://zenit.z.express/zh/components/badge) · [Tag](https://zenit.z.express/zh/components/tag) · [Chip](https://zenit.z.express/zh/components/chip) · [Card](https://zenit.z.express/zh/components/card) · [Alert](https://zenit.z.express/zh/components/alert) … |
| 导航 | 4 | [Tabs](https://zenit.z.express/zh/components/tabs) · [Accordion](https://zenit.z.express/zh/components/accordion) · [Menu](https://zenit.z.express/zh/components/menu) · [DropdownMenu](https://zenit.z.express/zh/components/dropdown) |
| 浮层与反馈 | 5 | [Notifier](https://zenit.z.express/zh/components/notification) · [Tooltip](https://zenit.z.express/zh/components/tooltip) · [Popover](https://zenit.z.express/zh/components/popover) · [Modal](https://zenit.z.express/zh/components/modal) · [Sheet](https://zenit.z.express/zh/components/sheet) |
| 布局与大数据 | 8 | [GlassBox](https://zenit.z.express/zh/components/glassbox) · [Divider](https://zenit.z.express/zh/components/divider) · [HStack / VStack](https://zenit.z.express/zh/components/stack) · [ui.box](https://zenit.z.express/zh/components/layoutbox) · [VirtualList](https://zenit.z.express/zh/components/virtuallist) … |
| 实验与能力验证 | 13 | [glasslab](https://zenit.z.express/zh/components/glasslab) · [glassislands](https://zenit.z.express/zh/components/glassislands) · [glassmotion](https://zenit.z.express/zh/components/glassmotion) · [glasschrome](https://zenit.z.express/zh/components/glasschrome) · [canvasevents](https://zenit.z.express/zh/components/canvasevents) … |

[Video](https://zenit.z.express/media/stories/button.mp4?v=6203a01415)

Button Story：扫过 variant × size 矩阵，再在 Primary 上展示 hover 与 press。完整说明见 [/components/button](https://zenit.z.express/zh/components/button)。

## Mount、状态与清理

多数可视组件采用构造器形式 `Config.mount(scope, cx)`：`ui.widgets.Button(props)` 返回一个 builder，`mount` 才真正建树。返回值可能是根 `*Node`，也可能是包含 wrapper、trigger、body、panel、state 等句柄的结果结构。

`mount_form.zig`

```zig
const EditorActions = struct {
    document: *Document,

    fn save(self: *EditorActions) void {
        self.document.save();
    }
};

const bindings = try cx.bindState(EditorActions, .{ .document = document });

const save = try ui.widgets.Button(.{
    .label = "Save",
    .variant = .primary,
    .on_click = cx.on(EditorActions, bindings, EditorActions.save),
}).mount(scope, cx);

const name = try ui.widgets.Input(.{
    .label_text = "Project name",
    .placeholder = "Untitled",
    .required = true,
    .width = 320,
}).mount(scope, cx);

try form.appendChild(cx.allocator, name);
try form.appendChild(cx.allocator, save);
```

少数组件是函数形式，props、scope 与 cx 一次传入：`mountScrollArea`、`mountGrid`，以及 `Select`、`ComboBox`、`TagsInput`、`NumberStepper`、`FileUpload`、`DataTable`（它们在 `ui.widgets` 中是 `mountX` 函数的别名，写作 `ui.widgets.Select(props, scope, cx)`）。

`scroll_area.zig`

```zig
// Function form: props, scope and cx in one call; returns a handle struct.
const area = try ui.widgets.mountScrollArea(.{ .height = 320 }, scope, cx);
try area.content.appendChild(cx.allocator, list);
try root.appendChild(cx.allocator, area.container);
```

| 组件 | 返回 |
| --- | --- |
| `Button` · `Input` | `*Node` |
| `Modal` | `ModalResult{ overlay, dialog, body, portaled }` |
| `Tooltip` | `TooltipResult{ wrapper, trigger, content }` |
| `Select` | `SelectMount{ wrapper, trigger, panel, state, is_open }` |
| `mountScrollArea` | `ScrollAreaResult{ container, content, state }` |
| `mountGrid` | `GridResult{ root, state }` |

不要把 config 当成之后还能写入的 live props。组件在 mount 时读取 config 建立节点和状态——在 config 里写 `signal.get()` 只会读到一次快照。持续变化的数据要通过返回的 state、Signal（例如 Modal 的 `.visible(sig)`）或组件公开的方法进入，也不要跨帧保存临时 config 的指针。组件内部状态挂在 mount 创建的子 Scope 上，父 Scope dispose 时一起释放。

## 浮层为什么必须用组件

Tooltip、Popover、Menu、Modal、Sheet 以及应用内提醒 [Notifier](https://zenit.z.express/zh/components/notification) 都通过 `OverlayStack` 注册一层浮层。带 barrier 的层（Modal / Sheet）会被挂到窗口级 portal（`cx.root` 下的 `WindowOverlayPortal`），从而逃出祖先的 overflow 裁剪。层的 z 值按语义 tier 自动分配：overlay 100、dialog 1000、toast 2000（Notifier 所在层）、tooltip 3000，devtools overlay 固定 32000；从浮层里再打开的浮层总是压过宿主层。给一个 box 设 absolute 和高 z-index 得不到这些行为。

`modal.zig`

```zig
const visible = try scope.createSignal(bool, false);

const modal = try ui.widgets.Modal(.{
    .title = "Delete document?",
    .width = 420,
}).visible(visible).mount(scope, cx);

try modal.body.appendChild(cx.allocator, confirm_content);
// modal.portaled == true: the barrier already lives under the window-level
// portal. Do not append modal.overlay to the current tree.

// Open it from any handler:
visible.set(true);
```

OVERLAY PORTAL

mount 的位置不决定浮层在哪里绘制：barrier 进入 portal，z 由 tier 决定，Escape 从栈顶向下找第一个可关闭的层。

-   **内部点击**留在 dialog / panel 内，不触发 outside-dismiss；点击 barrier 按 `close_on_overlay` 关闭。
    
-   **Escape** 只关闭最上层浮层，嵌套层级逐级退栈。
    
-   **焦点**：Modal 打开时 trap 焦点并自动聚焦，关闭后还原到之前的位置。
    
-   **Scope dispose** 会主动从 portal 摘除 barrier 子树，切换页面后不会留下透明命中层。
    
-   **退场动画**期间浮层保持可见，动画提交后才挂起，而不是直接跳到终点。
    

不阻塞的全局提醒用 `ui.widgets.Notifier`（取代了旧的 ToastManager）：`Notifier.init(scope, cx, .{})` 在 toast tier 注册一层，并在窗口 portal 存在时把整窗容器挂进去（`portaled` 为 false 时自己把 `container` 挂到根上）；之后 `show(.{ .tone = .success, .title = "Saved" })` 返回 id，`update` 原地改写、`dismiss` 收起。关闭、到期和按钮操作通过 `Listener` 事件回到应用；历史与勿扰等通知中心逻辑由应用自己实现。

[Video](https://zenit.z.express/media/stories/modal.mp4?v=f1b080996c)

Harness 打开居中 dialog，并验证内部点击不会误关。[Modal](https://zenit.z.express/zh/components/modal)

[Video](https://zenit.z.express/media/stories/tooltip.mp4?v=64c28daff2)

虚拟鼠标悬停 trigger，气泡进入 tooltip tier，不改变布局树。[Tooltip](https://zenit.z.express/zh/components/tooltip)

## Showcase 同时是一组回归测试

`e2e/storybook.test.ts` 按 `nav.<key>` 切换每个 Story，先收集子树文本并断言预期数据值全部出现，再截图；交互组件还会断言交互后的状态、几何或像素。组件站的录像复用同一个应用和同一组语义 id，所以「演示」和「测试」不会分叉成两套实现。

| 门禁 | 能抓住的问题 |
| --- | --- |
| 文本 / 状态回读 | 空面板、回调未写回、过滤 / 分页值错误 |
| 几何断言 | padding box、portal 居中、Sheet 贴边、拖拽位移错误 |
| 像素断言 | GPU 层空白、blend 退化、emoji 灰度化、z 顺序覆盖错误 |
| 录像 | hover、press、光标、IME、滚动与入场动画的时间行为 |

> WARNING
> 
> **Storybook 不等于业务组件。** 编辑器、文件浏览器、命令面板和领域数据模型属于应用层。公共组件库只承诺可复用的行为与视觉原语。
