---
title: "Composant Notification — zenit Zig UI"
description: "Notifications non bloquantes : neuf types, progression, actions, empilement et huit positions d'ancrage."
url: https://zenit.z.express/fr/components/notification
language: fr
alternate_en: https://zenit.z.express/components/notification.md
alternate_zh: https://zenit.z.express/zh/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_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`

Notifications non bloquantes : neuf types, progression, actions, empilement et huit positions d'ancrage.

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

**BEHAVIOR CONTRACT**

Le Notifier gère la pile du portal, l'entrée / la sortie, le déploiement au survol avec minuteurs en pause, et garde la pile ancrée lors du redimensionnement ; les cartes utilisent un matériau de verre blanc.

**RECORDED INTERACTION**

Le harness déclenche les neuf types (en laissant la carte de progression aller au bout), en envoie quatre d'affilée pour former une pile, la déploie au survol, ferme la carte du milieu, puis les efface toutes.

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

Huit positions d'ancrage : changer de position déplace l'ancre, le sens de croissance et le sens d'entrée sans reconstruire les cartes. Cadrage sur toute la fenêtre, car les positions de gauche longent le bord de la fenêtre.

[Lire le guide →](https://zenit.z.express/fr/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;
}
```

[Voir le code sur GitHubexamples/storybook/notification\_story.zig:290](https://github.com/version-next/zenit/blob/HEAD/examples/storybook/notification_story.zig#L290)

Le code source tel quel de la story dans le Storybook (zenit 5f9add5+wip 2026-09-30). `W` désigne `ui.widgets` ; `col` / `row` / `label` sont de petits helpers de mise en page du 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)

| Champ | Type | Défaut | Description |
| --- | --- | --- | --- |
| `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)

| Champ | Type | Défaut | Description |
| --- | --- | --- | --- |
| `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
```

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