Notifications
Use the Notifier for in-window notifications that never block the task at hand (success, error, progress, undo and messages). Stacking, hover-to-expand and eight docking positions are handled for you.
What the Notifier is
ui.widgets.Notifier is a window-level service: initialize it once, then call show from anywhere. Cards live on the toast tier of the window overlay portal (z 2000: above dialogs, below tooltips) and never take part in page layout. Each card owns a fixed node, so removing one never rebuilds the others or replays their entrance. It replaces the earlier ToastManager.
The field-by-field API (Notification, NotifierOptions and every method signature) is on the component site’s Notification page.
Creating a Notifier
Create it once in your root mount function and hand the pointer to whatever needs to notify (for example, app state held by cx.bindState). The Notifier is owned by the Scope you pass and goes away with it.
const notifier = try ui.widgets.Notifier.init(scope, cx, .{
.position = .bottom_center,
// Keep the stack clear of a 56px toolbar.
.content_insets = .{ .top = 56 },
.max_visible = 4,
});positionOne of eight positions, default .bottom_center; switch at runtime with setPositionmax_visibleHow many cards show when expanded (2–6, default 4)expand_on_hoverWhen false, hover only pauses timers and does not expandcontent_insetsInsets from the window edges (title bar, toolbar, status bar); the 22px docking margin starts herewidthCard width, default 392stringsUI strings ("now", "Undo", "Reply…", the collapsed badge …) for localizationlistenerCallback for user actions and expiry; also settable with setListenerShowing a notification
show takes a ui.widgets.Notification value and returns its id. The Notifier copies every string, so your buffers can be reused right away. Beyond six cards on screen, the oldest leaves with its normal exit animation.
// Success: auto-dismisses after 3.6 s.
_ = try notifier.show(.{
.tone = .success,
.title = "Saved to iCloud",
.body = "3 files synced · 2.1 MB",
});
// Error with two actions: errors stay until the user acts.
const failed = try notifier.show(.{
.tone = .@"error",
.title = "Upload failed",
.body = "network timeout · dist/app.zip",
.actions = &.{
.{ .label = "Retry", .primary = true, .tag = "retry" },
.{ .label = "View log", .tag = "log" },
},
});
// Undo: a 7 s countdown ring and a default "Undo" button.
_ = try notifier.show(.{
.kind = .undo,
.title = "Moved 4 items to the Trash",
.body = "You can undo this for 7 seconds.",
});
// A person's message with an inline reply box.
_ = try notifier.show(.{
.tone = .violet,
.avatar = "L",
.title = "Lin",
.body = "I finished the table widths in section three.",
.reply = true,
});Kinds, tones and lifetimes
A card’s look comes from two axes: kind (layout) and tone (semantic color). The leading icon is derived from both, or set explicitly with lead / glyph.
.defaultIcon, title and body, with optional buttons, reply box or avatar.progressSpinner, determinate bar and an n / N count; no life bar.undoCountdown ring showing seconds left, plus a default "Undo" button.successSomething finished3.6 s.@"error"A failure that needs attentionsticky.warningWorth noticing, not blocking5.6 s.infoGeneral information (default)4.2 s.violetPeople, invitations and other social messages4.2 s; sticky with buttons.quietLow-key status (a small dot instead of an icon)2.8 s- ✓
Sticky by default: progress cards, cards with a reply box, error tones and violet cards with buttons never auto-dismiss.
- ✓
Explicit overrides:
sticky = trueforces it;duration_msreplaces the default;life_bartoggles the lifetime bar. - ✓
Undo defaults to 7 seconds, with the ring tracking the timer.
In-place updates and progress
update(id, …) rewrites the same card’s kind, tone, title and body and restarts its lifetime: no destroy, no rebuild, no replayed entrance. Drive a progress card with setProgress, which touches only the progress layer. Calling show with an existing id also transforms in place.
const id = try notifier.show(.{
.kind = .progress,
.title = "Uploading attachment",
.body = "assets/hero-shot.png",
.progress = .{ .value = 0, .label = "Uploading · about 5 s", .count = "0 / 5" },
});
// Each tick: only the progress layer is touched, the card isn't rebuilt.
notifier.setProgress(id, 0.6, "Uploading · about 2 s", "3 / 5");
// Done: the same card turns into a success card in place,
// no new entrance, and it now dismisses itself after 3.4 s.
try notifier.update(id, .{
.tone = .success,
.title = "Upload complete",
.body = "assets/hero-shot.png",
.duration_ms = 3400,
});Handling user actions
The Notifier reports through a single ui.widgets.NotificationListener. A NotificationEvent carries the card id, the event kind, and the button tag / index or the reply text.
.actionA button was clicked (tell them apart by tag).undoUndo was clicked; the ring has already turned into a ✓.replyThe reply box was submitted (Enter or Send); text holds it.dismissedThe user closed it (✕ or a fling).expiredIt timed outconst Uploads = struct {
fn onEvent(ctx: ?*anyopaque, n: *ui.widgets.Notifier, e: ui.widgets.NotificationEvent) void {
const self: *Uploads = @ptrCast(@alignCast(ctx orelse return));
switch (e.kind) {
.action => if (std.mem.eql(u8, e.tag, "retry")) {
// Turn the sticky error card into a progress card in place.
n.update(e.id, .{
.kind = .progress,
.title = "Retrying upload",
.progress = .{ .value = 0, .label = "Uploading", .count = "0 / 5" },
}) catch return;
self.restart(e.id);
} else n.dismiss(e.id),
.reply => self.send(e.text),
.undo => self.restoreTrash(),
.dismissed, .expired => {},
}
}
// …restart / send / restoreTrash
};
const uploads = try cx.bindState(Uploads, .{});
notifier.setListener(.{ .context = @ptrCast(uploads), .callback = Uploads.onEvent });Stacking and hover
Several notifications collapse into a stack: the newest sits closest to the anchor with the highest z; each layer behind peeks out 9px and shrinks 3.8%, up to three layers, with an "N more · hover to expand" badge. When the pointer enters the stack’s bounds (grown by 18px), it expands with 9px gaps and every timer pauses; leaving collapses it and resumes.
Next to the badge sits a clear-all button: the first click reveals its label, the second actually clears, and moving away folds it back. In code, call dismissAll().
A single card can be flung away, which reports dismissed.
On window resize the stack follows its anchor rigidly, with no trailing tween.
Positions
setPosition changes only the anchor, growth direction and entrance offset. Cards aren’t rebuilt, and any card that is leaving finishes its exit in the new direction.
The notification center is yours
Notification-center features (history, do-not-disturb, a bell entry point) are app logic and live outside zenit: record history where you call show, decide whether to pop a card at all, and learn about dismissals, expiry and button actions through the Listener. The Storybook’s Digest card shows the pattern: after do-not-disturb ends, one quiet card summarizes what arrived.