---
title: "Notification 组件 — zenit Zig UI"
description: "不打断当前任务的系统级通知：九种类型、进度、操作按钮、堆叠与八个停靠位置。Notifier 管理 portal 栈、入场 / 退场、悬停展开与暂停计时、窗口尺寸变化时堆叠跟随…"
url: https://zenit.z.express/zh/components/notification
language: zh-CN
alternate_en: https://zenit.z.express/components/notification.md
alternate_es: https://zenit.z.express/es/components/notification.md
alternate_ja: https://zenit.z.express/ja/components/notification.md
alternate_ko: https://zenit.z.express/ko/components/notification.md
alternate_fr: https://zenit.z.express/fr/components/notification.md
alternate_de: https://zenit.z.express/de/components/notification.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# Notification

`ui.widgets.Notifier`

不打断当前任务的系统级通知：九种类型、进度、操作按钮、堆叠与八个停靠位置。

[Video](https://zenit.z.express/media/stories/notification.mp4?v=5f8cadb85b)

**BEHAVIOR CONTRACT**

Notifier 管理 portal 栈、入场 / 退场、悬停展开与暂停计时、窗口尺寸变化时堆叠跟随锚点；卡片使用白玻璃材质。

**RECORDED INTERACTION**

Harness 依次触发全部九种类型（进度卡跑完到成功），再连发 4 条形成堆叠，悬停展开并关闭中间一条，最后全部收起。

[Video](https://zenit.z.express/media/stories/notification-positions.mp4?v=2df7ff87b6)

八个停靠位置：切换位置只改锚点、增长方向与入场方向，卡片不重建。整窗画面，因为左侧停靠位贴着窗口左缘。

[阅读使用指南 →](https://zenit.z.express/zh/docs/advanced/notifications)

`examples/storybook/notification_story.zig:290`

```zig
pub fn build(scope: *ui.Scope, cx: *ui.Cx) anyerror!*ui.Node {
    const t = cx.tokens;
    const root = try ui.box(cx, .{ .width = .{ .grow = .{} }, .direction = .column, .gap = 12 }, .{});

    const n = try Notifier.init(scope, cx, .{
        .position = .bottom_center,
        // storybook 顶部 56px 工具栏。
        .content_insets = .{ .top = 56 },
        .strings = zh_strings,
    });
    const status = try ui.text(cx, "event · idle", .{ .font_size = 12, .font_weight = 550, .color = t.color.fg_secondary });
    status.meta.ownership.meta.test_id = "story.notify.status";
    const state = try cx.bindState(Story, .{ .n = n, .cx = cx, .status = status });
    n.setListener(.{ .context = @ptrCast(state), .callback = Story.onEvent });
    root.meta.per_frame.hooks.slots.anim_state = @ptrCast(state);
    root.meta.per_frame.hooks.before_render.main = storyHook;

    const kinds = try controlRow(cx);
    try addButton(scope, cx, kinds, state, "成功 Success", "story.notify.btn.success", Story.success, .secondary);
    try addButton(scope, cx, kinds, state, "错误 Error", "story.notify.btn.error", Story.failure, .secondary);
    try addButton(scope, cx, kinds, state, "警告 Warning", "story.notify.btn.warning", Story.warning, .secondary);
    try addButton(scope, cx, kinds, state, "进行中 Progress", "story.notify.btn.progress", Story.progress, .secondary);
    try addButton(scope, cx, kinds, state, "需要操作 Action", "story.notify.btn.action", Story.action, .secondary);
    try addButton(scope, cx, kinds, state, "人员消息 Person", "story.notify.btn.person", Story.person, .secondary);
    try addButton(scope, cx, kinds, state, "撤销 Undo", "story.notify.btn.undo", Story.undo, .secondary);
    try addButton(scope, cx, kinds, state, "静默 Quiet", "story.notify.btn.quiet", Story.quiet, .secondary);
    try addButton(scope, cx, kinds, state, "汇总 Digest", "story.notify.btn.digest", Story.digest, .secondary);
    try root.appendChild(cx.allocator, try group(cx, try sectionLabel(cx, "九种类型 Nine kinds", "卡片 392 宽 · 圆角 18 · 液态玻璃；按钮在分隔线下方，中性灰底不上色。"), kinds));

    const stack = try controlRow(cx);
    try addButton(scope, cx, stack, state, "连发 4 条", "story.notify.btn.burst", Story.burst, .primary);
    try addButton(scope, cx, stack, state, "全部收起", "story.notify.btn.clear", Story.clear, .ghost);
    try stack.appendChild(cx.allocator, status);
    try root.appendChild(cx.allocator, try group(cx, try sectionLabel(cx, "堆叠 Stack", "折叠露边 9px · 悬停展开并暂停所有计时 · 删除中间一条平滑补位。"), stack));

    const pos = try controlRow(cx);
    try addButton(scope, cx, pos, state, "上左", "story.notify.pos.tl", Story.posTL, .secondary);
    try addButton(scope, cx, pos, state, "上中", "story.notify.pos.tc", Story.posTC, .secondary);
    try addButton(scope, cx, pos, state, "上右", "story.notify.pos.tr", Story.posTR, .secondary);
    try addButton(scope, cx, pos, state, "右中", "story.notify.pos.rc", Story.posRC, .secondary);
    try addButton(scope, cx, pos, state, "右下", "story.notify.pos.br", Story.posBR, .secondary);
    try addButton(scope, cx, pos, state, "下中", "story.notify.pos.bc", Story.posBC, .secondary);
    try addButton(scope, cx, pos, state, "下左", "story.notify.pos.bl", Story.posBL, .secondary);
    try addButton(scope, cx, pos, state, "左中", "story.notify.pos.lc", Story.posLC, .secondary);
    try root.appendChild(cx.allocator, try group(cx, try sectionLabel(cx, "八个停靠位置 Positions", "位置只改锚点、增长方向、入场位移方向；切换不重建卡片。"), pos));

    // 玻璃需要有内容的背景才看得出来：模拟卡片身后的正文（设计稿里提醒浮在编辑器正文上）。
    const backdrop = try ui.box(cx, .{ .width = .{ .grow = .{} }, .direction = .column, .gap = 0 }, .{});
    backdrop.meta.ownership.meta.test_id = "story.notify.backdrop";
    const tints = [_]u24{ 0xE4572E, 0x17BEBB, 0xFFC914, 0x2E282A, 0x76B041, 0x8059BB };
    var li: usize = 0;
    while (li < 36) : (li += 1) {
        const line = try ui.box(cx, .{ .width = .{ .grow = .{} }, .height = .{ .px = 22 }, .direction = .row, .gap = 28, .padding = ui.Padding.symmetric(0, 12), .align_items = .center, .background = ui.arb.hex(tints[li % tints.len]) }, .{});
        var k: usize = 0;
        while (k < 6) : (k += 1) try line.appendChild(cx.allocator, try ui.text(cx, "The foundation of any great editor", .{ .font_size = 13, .color = ui.arb.hex(0xFFFFFF) }));
        try backdrop.appendChild(cx.allocator, line);
    }
    try root.appendChild(cx.allocator, backdrop);

    if (!n.portaled) try root.appendChild(cx.allocator, n.container);
    return root;
}
```

[在 GitHub 查看源码examples/storybook/notification\_story.zig:290](https://github.com/version-next/zenit/blob/HEAD/examples/storybook/notification_story.zig#L290)

这是 Storybook 中该 story 的原样代码（zenit 5f9add5+wip 2026-09-30）。其中 `W` 即 `ui.widgets`，`col` / `row` / `label` 是 Storybook 内的布局小工具。

### Notification

`ui.widgets.Notification`

[src/ui/components/notification/mod.zig:51](https://github.com/version-next/zenit/blob/HEAD/src/ui/components/notification/mod.zig#L51)

| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `id` | `?Id` | `null` | 已存在的 id 会原地转换（保留 born，不重放入场）。 |
| `kind` | `Kind` `.default` `.progress` `.undo` | `.default` | — |
| `tone` | `Tone` `.success` `.@"error"` `.warning` `.info` `.violet` `.quiet` | `.info` | — |
| `title` | `[]const u8` | `""` | — |
| `body` | `[]const u8` | `""` | — |
| `glyph` | `?svg_assets.Asset` | `null` | 覆盖语义色调的默认字形（✓ / ✕ / △ …）。 |
| `lead` | `?Lead` `.chip` `.spinner` `.ring` `.avatar` `.dot` | `null` | 覆盖行首图标位的默认呈现（由 kind / tone / avatar 推导）。 |
| `avatar` | `[]const u8` | `""` | 人员消息的头像首字。 |
| `actions` | `[]const Action` | `&.{}` | 分隔线下方的等宽按钮（最多 2 个）。undo 类型缺省为「撤销」。 |
| `reply` | `bool` | `false` | 内联回复框（Enter 或「发送」提交）。 |
| `quote` | `?[]const u8` | `null` | 回复已发出后的引用行（原地转换用）。 |
| `progress` | `?Progress` | `null` | — |
| `sticky` | `bool` | `false` | 常驻：不自动消失。 |
| `duration_ms` | `?u32` | `null` | 覆盖类型默认停留时长。 |
| `life_bar` | `?bool` | `null` | 覆盖生命条显示（默认：有时限且非 progress 时显示）。 |

### Options

`ui.widgets.NotifierOptions`

[src/ui/components/notification/mod.zig:112](https://github.com/version-next/zenit/blob/HEAD/src/ui/components/notification/mod.zig#L112)

| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `position` | `Position` `.top_left` `.top_center` `.top_right` `.right_center` `.bottom_right` `.bottom_center` `.bottom_left` `.left_center` | `.bottom_center` | — |
| `max_visible` | `u8` | `Spec.default_max_visible` | 展开时最多显示条数（2–6）。 |
| `expand_on_hover` | `bool` | `true` | false：悬停只暂停计时，不展开。 |
| `content_insets` | `Insets` | `.{}` | 内容区相对窗口的内缩（标题栏 / 状态栏），停靠边距 22 从这里起算。 |
| `width` | `f32` | `Spec.card_width` | — |
| `strings` | `Strings` | `.{}` | — |
| `listener` | `?Listener` | `null` | — |

### Notifier

`ui.widgets.Notifier`

[src/ui/components/notification/mod.zig:132](https://github.com/version-next/zenit/blob/HEAD/src/ui/components/notification/mod.zig#L132)

```zig
fn init(scope: *Scope, cx: *Cx, options: Options) !*Notifier
```

```zig
fn show(self: *Notifier, n: Notification) !Id
```

弹出一条提醒；id 已存在时原地转换。同屏超过 6 条时最旧一条走正常退场。

```zig
fn update(self: *Notifier, id: Id, n: Notification) !void
```

原地转换：改写 kind / tone / title / body，重置 life，换图标 —— 不销毁、 不重建卡片，born 不变，不重放入场（16.6）。

```zig
fn setProgress(self: *Notifier, id: Id, value: f32, label: ?[]const u8, count_text: ?[]const u8) void
```

更新进度卡的进度（不重建内容层）。

```zig
fn dismiss(self: *Notifier, id: Id) void
```

```zig
fn dismissAll(self: *Notifier) void
```

```zig
fn setPosition(self: *Notifier, position: Position) void
```

位置切换不重建卡片，只改锚点参数；退场中的沿新方向继续完成退场。

```zig
fn setListener(self: *Notifier, listener: ?Listener) void
```

```zig
fn setExpandOnHover(self: *Notifier, on: bool) void
```

```zig
fn setMaxVisible(self: *Notifier, n: u8) void
```

```zig
fn count(self: *const Notifier) usize
```

在场（未退场）的条数。

```zig
fn isExpanded(self: *const Notifier) bool
```

```zig
fn isPaused(self: *const Notifier) bool
```

```zig
fn cardForId(self: *Notifier, id: Id) ?*Card
```

取卡片（测试 id、埋点、截图断言用）。

```zig
fn clearAll(self: *Notifier) void
```

用户「全部清除」：所有在屏提醒按用户关闭退场（逐条回调 `.dismissed`）。

```zig
fn replyFailed(self: *Notifier, id: Id, title: []const u8, body: []const u8) void
```

回复发送失败：原地转为错误类型并把刚才的内容恢复进回复框。
