Notification
A toast notification system for displaying temporary messages to users. Notifications appear at the top right of the window and can auto-dismiss after a timeout. Supports multiple variants (info, success, warning, error), custom content, titles, and action buttons. Perfect for status updates, confirmations, and user feedback.
#Import
use gpui_kit::component::{
notification::{Notification, NotificationType},
WindowExt
};// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { Notice, NotificationPlacement, Notification, NotificationLayer, NotificationStack } from "notification.slint";use iced::widget::{column, container, row, scrollable, stack, svg, text};
use iced::{Alignment, Element, Length, Task, Theme};#Usage
#Where notifications render
The window’s Root automatically mounts and renders notifications. Open the window with gpui_kit::open_window, or wrap the application view in Root::new. Application views do not render overlay layers themselves.
#Basic Notification
// Simple string notification
window.push_notification("This is a notification.", cx);
// Using Notification builder
Notification::new()
.message("Your changes have been saved.")Notification {
title: "Saved";
message: "Your changes are on the server.";
}// The whole card: the status mark, a title and a message. The dismiss
// control is drawn because the card is closable, which is the Slint default.
let basic = Notification::new()
.title("Saved")
.message("Your changes are on the server.")
.on_close(Message::DismissStatic);#Notification Types
// Info notification (blue)
window.push_notification(
(NotificationType::Info, "File saved successfully."),
cx,
);
// Success notification (green)
window.push_notification(
(NotificationType::Success, "Payment processed successfully."),
cx,
);
// Warning notification (yellow/orange)
window.push_notification(
(NotificationType::Warning, "Network connection is unstable."),
cx,
);
// Error notification (red)
window.push_notification(
(NotificationType::Error, "Failed to save file. Please try again."),
cx,
);VerticalLayout {
spacing: 12px;
alignment: start;
Notification {
status: Status.info;
title: "Update available";
message: "Version 0.6.5 is ready to install.";
}
Notification {
status: Status.success;
title: "Deployed";
message: "The build is live.";
}
Notification {
status: Status.warning;
title: "Slow request";
message: "The last query took 4.2s.";
}
Notification {
status: Status.danger;
title: "Upload failed";
message: "The connection dropped.";
}
}// `status` picks the glyph and the colour; the layout is the same for all
// four.
let types = column![
Notification::new()
.status(Status::Info)
.title("Update available")
.message("Version 0.6.5 is ready to install.")
.on_close(Message::DismissStatic),
Notification::new()
.status(Status::Success)
.title("Deployed")
.message("The build is live.")
.on_close(Message::DismissStatic),
Notification::new()
.status(Status::Warning)
.title("Slow request")
.message("The last query took 4.2s.")
.on_close(Message::DismissStatic),
Notification::new()
.status(Status::Danger)
.title("Upload failed")
.message("The connection dropped.")
.on_close(Message::DismissStatic),
]
.spacing(theme::SPACING_MD)
.align_x(Alignment::Start);#Notification with Title
Notification::new()
.title("Update Available")
.message("A new version of the application is ready to install.")
.with_type(NotificationType::Info)Notification {
title: "Copied to clipboard";
message: "";
}// A title with no message: the body column simply omits the line.
let titled = Notification::new()
.title("Copied to clipboard")
.on_close(Message::DismissStatic);#Auto-hide Control
// Disable auto-hide (manual dismiss only)
Notification::new()
.message("This notification stays until manually closed.")
.autohide(false)
// Default auto-hide after 5 seconds
Notification::new()
.message("This will disappear automatically.")
.autohide(true) // defaultVerticalLayout {
spacing: 12px;
alignment: start;
Notification {
title: "Closable";
message: "This one has a dismiss button.";
closable: true;
}
Notification {
title: "Sticky";
message: "No dismiss button; the application decides.";
closable: false;
}
}// A closable card carries a dismiss control; a sticky one does not, and the
// application decides when it goes.
let timing = column![
Notification::new()
.title("Closable")
.message("This one has a dismiss button.")
.on_close(Message::DismissStatic),
Notification::new()
.title("Sticky")
.message("No dismiss button; the application decides.")
.closable(false),
]
.spacing(theme::SPACING_MD)
.align_x(Alignment::Start);The countdown pauses while the pointer is over the notifications or one of them has keyboard focus, and resumes when the pointer leaves or focus moves on. It keeps running while the window is inactive, so a message that must not be missed should disable auto-hide or use system delivery.
#Placement
// The six anchors. Pick one and press Show: the stack is packed against that
// corner of the frame.
let anchors = [
(Placement::TopLeft, "Top left"),
(Placement::TopCenter, "Top centre"),
(Placement::TopRight, "Top right"),
(Placement::BottomLeft, "Bottom left"),
(Placement::BottomCenter, "Bottom centre"),
(Placement::BottomRight, "Bottom right"),
];
let picker = row(anchors.map(|(placement, label)| -> Element<'_, Message> {
Button::new(label)
.size(theme::ControlSize::XSmall)
.selected(app.placement == placement)
.on_press(Message::SetPlacement(placement))
.into()
}))
.spacing(theme::SPACING_XS);
let placed_layer: Element<'_, Message> = if app.placed {
NotificationLayer::new()
.placement(app.placement)
.item(
Notification::new()
.title("Placed")
.message("The stack sits against the chosen corner.")
.on_close(Message::DismissStatic),
)
.into()
} else {
NotificationLayer::new().placement(app.placement).into()
};
let placement = column![
picker,
row![
Button::new("Show")
.variant(Variant::Primary)
.on_press(Message::ShowPlaced),
Button::new("Clear").on_press(Message::ClearPlaced),
]
.spacing(theme::SPACING_SM),
// The layer is always mounted, so the frame keeps its shape and the
// stack appears at the corner the buttons name.
framed(placed_layer, FRAME_HEIGHT),
text("The frame is the caller's; the layer fills it and packs the stack into one corner.")
.size(theme::FONT_SIZE_XS)
.style(|theme: &Theme| text::Style {
color: Some(Tokens::of(theme).muted_foreground),
}),
]
.spacing(theme::SPACING_MD)
.align_x(Alignment::Start);Notifications appear at the top right of the window by default. Set a global default for all notifications, or override it for a single notification. Notifications are stacked separately for each placement.
use gpui_kit::Anchor;
// Global default (default: Anchor::TopRight)
Theme::update(cx, |theme| theme.notification.placement = Anchor::BottomRight);
// Per-notification override
Notification::info("Download complete.")
.placement(Anchor::BottomLeft)VerticalLayout {
spacing: 12px;
HorizontalLayout {
horizontal-stretch: 1;
spacing: 6px;
Button {
text: "Top left";
size: ControlSize.xsmall;
selected: root.place == NotificationPlacement.top-left;
clicked => {
root.place = NotificationPlacement.top-left;
}
}
Button {
text: "Top centre";
size: ControlSize.xsmall;
selected: root.place == NotificationPlacement.top-center;
clicked => {
root.place = NotificationPlacement.top-center;
}
}
Button {
text: "Top right";
size: ControlSize.xsmall;
selected: root.place == NotificationPlacement.top-right;
clicked => {
root.place = NotificationPlacement.top-right;
}
}
Button {
text: "Bottom left";
size: ControlSize.xsmall;
selected: root.place == NotificationPlacement.bottom-left;
clicked => {
root.place = NotificationPlacement.bottom-left;
}
}
Button {
text: "Bottom centre";
size: ControlSize.xsmall;
selected: root.place == NotificationPlacement.bottom-center;
clicked => {
root.place = NotificationPlacement.bottom-center;
}
}
Button {
text: "Bottom right";
size: ControlSize.xsmall;
selected: root.place == NotificationPlacement.bottom-right;
clicked => {
root.place = NotificationPlacement.bottom-right;
}
}
}
HorizontalLayout {
spacing: 8px;
alignment: start;
Button {
text: "Show";
variant: ButtonVariant.primary;
clicked => {
root.placed = [
{ title: "Placed", message: "The stack slides in from the chosen edge.", status: Status.info }
];
}
}
Button {
text: "Clear";
clicked => {
root.placed = [
];
}
}
}
Rectangle {
width: 100%;
height: 320px;
border-radius: Theme.radius-lg;
border-width: 1px;
border-color: Theme.border;
background: Theme.muted.with-alpha(0.25);
clip: true;
// Always mounted: the layer measures its corner against this
// frame, and it slides in whenever `placed` gains an item.
NotificationLayer {
items: root.placed;
placement: root.place;
dismissed(index) => {
root.placed = [
];
}
}
}
}Supported values are Anchor::TopLeft, Anchor::TopCenter,
Anchor::TopRight, Anchor::LeftCenter, Anchor::RightCenter,
Anchor::BottomLeft, Anchor::BottomCenter, and Anchor::BottomRight.
#With Action Button
Notification::new()
.title("Connection Lost")
.message("Unable to connect to server.")
.with_type(NotificationType::Error)
.autohide(false)
.action(|_, cx| {
Button::new("retry")
.primary()
.label("Retry")
.on_click(cx.listener(|this, _, window, cx| {
// Perform retry action
println!("Retrying connection...");
this.dismiss(window, cx);
}))
})VerticalLayout {
spacing: 12px;
alignment: start;
Notification {
status: Status.warning;
title: "Unsaved changes";
message: "Leaving now discards the draft.";
}
HorizontalLayout {
spacing: 8px;
alignment: start;
Button {
text: "Discard";
outline: true;
}
Button {
text: "Keep editing";
variant: ButtonVariant.primary;
}
}
}// A notice and the controls for it. Slint places the action row under the
// card, so the caller owns it here too.
let action = column![
Notification::new()
.status(Status::Warning)
.title("Unsaved changes")
.message("Leaving now discards the draft.")
.on_close(Message::DismissStatic),
row![
Button::new("Discard")
.outline()
.on_press(Message::DismissStatic),
Button::new("Keep editing")
.variant(Variant::Primary)
.on_press(Message::DismissStatic),
]
.spacing(theme::SPACING_SM),
]
.spacing(theme::SPACING_MD)
.align_x(Alignment::Start);#Clickable Notifications
Notification::new()
.message("Click to view details")
.on_click(cx.listener(|_, _, _, cx| {
println!("Notification clicked");
// Handle notification click
cx.notify();
}))VerticalLayout {
spacing: 12px;
HorizontalLayout {
spacing: 8px;
alignment: start;
Button {
text: "Success";
icon: "circle-check";
clicked => {
root.clicked = [
{ title: "Deployed", message: "The build is live.", status: Status.success }
];
}
}
Button {
text: "Info";
icon: "info";
clicked => {
root.clicked = [
{ title: "Update available", message: "Version 0.6.5 is ready.", status: Status.info }
];
}
}
Button {
text: "Warning";
icon: "triangle-alert";
clicked => {
root.clicked = [
{ title: "Slow request", message: "The last query took 4.2s.", status: Status.warning }
];
}
}
Button {
text: "Error";
icon: "circle-x";
clicked => {
root.clicked = [
{ title: "Upload failed", message: "The connection dropped.", status: Status.danger }
];
}
}
}
Rectangle {
width: 100%;
height: 240px;
border-radius: Theme.radius-lg;
border-width: 1px;
border-color: Theme.border;
background: Theme.muted.with-alpha(0.25);
clip: true;
if root.clicked.length == 0: Text {
text: "Press a button to raise a notification.";
color: Theme.muted-foreground;
font-size: Theme.font-size-xs;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
NotificationLayer {
items: root.clicked;
placement: NotificationPlacement.top-right;
dismissed(index) => {
root.clicked = [
];
}
}
}
}// A button raises a notice, and the notice's dismiss control removes it from
// the caller's list.
let raisers = [
(Status::Success, "Success"),
(Status::Info, "Info"),
(Status::Warning, "Warning"),
(Status::Danger, "Error"),
];
let raiser_buttons = row(
raisers.map(|(status, label)| -> Element<'_, Message> {
Button::new(label).on_press(Message::Raise(status)).into()
}),
)
.spacing(theme::SPACING_SM);
let raised_layer: Element<'_, Message> = match app.raised {
Some(status) => NotificationLayer::new()
.item(
Notification::new()
.status(status)
.title(status_name(status))
.message(status_message(status))
.on_close(Message::DismissRaised),
)
.into(),
None => iced::widget::Space::new().into(),
};
let clickable = column![
raiser_buttons,
framed(
stack![empty_hint("Press a button to raise a notification."), raised_layer],
FRAME_HEIGHT
),
text("Press a button to raise a notice; press its dismiss control to take it down.")
.size(theme::FONT_SIZE_XS)
.style(|theme: &Theme| text::Style {
color: Some(Tokens::of(theme).muted_foreground),
}),
]
.spacing(theme::SPACING_MD)
.align_x(Alignment::Start);#Custom Content
use gpui_kit::component::text::markdown;
let markdown_content = r#"
### Form Validation Error
```rust
Notification::error("Please correct the following errors before submitting.")
.title("Validation Failed")
.autohide(false)
.action(|_, _, cx| {
Button::new("review")
.outline()
.label("Review Form")
.on_click(cx.listener(|this, _, window, cx| {
// Navigate to form
this.dismiss(window, cx);
}))
})#File Upload Progress
struct UploadNotification;
// Start upload notification
window.push_notification(
Notification::info("Uploading file...")
.id::<UploadNotification>()
.title("File Upload")
.autohide(false),
cx,
);
// Update to success when complete
window.push_notification(
Notification::success("File uploaded successfully!")
.id::<UploadNotification>()
.title("Upload Complete"),
cx,
);#System Status Updates
// Warning about maintenance
Notification::warning("System maintenance will begin in 30 minutes.")
.title("Scheduled Maintenance")
.autohide(false)
.action(|_, cx| {
Button::new("details")
.link()
.label("View Details")
.on_click(cx.listener(|this, _, window, cx| {
// Show maintenance details
this.dismiss(window, cx);
}))
})#Batch Operation Results
use gpui_kit::component::text::markdown;
let results_content = r#"
## Custom Notification
- **Feature**: New dashboard available
- **Status**: Ready to use
- [Learn more](https://example.com)
"#;
Notification::new()
.content(|_, window, cx| {
markdown(markdown_content).into_any_element()
})#Unique Notifications
When you need to manage notifications manually, such as for long-running processes or persistent alerts, you can use unique IDs to push and remove notifications as needed.
In this case, you can create a special struct in local scope, and use id methods with this struct to identify the notification.
Then you can push the notification when needed, and later remove it using the same ID.
Like this:
// Using type-based ID for uniqueness
struct UpdateNotification;
Notification::new()
.id::<UpdateNotification>()
.message("System update available")
.autohide(false)
// Using type + element ID for multiple unique notifications
struct TaskNotification;
Notification::warning("Task failed to complete")
.id1::<TaskNotification>("task-123")
.title("Task Failed")Then remove the notification with window.remove_notification::<UpdateNotification>, like this:
// Later, dismiss the notification
window.remove_notification::<UpdateNotification>(cx);#System Notification
A notification can also be delivered to the operating system’s notification
center. Use NotificationDelivery to choose where a notification goes: as an
in-app toast (InApp, the default), in the OS notification center (System),
or both (InAppAndSystem).
use gpui_kit::component::notification::{Notification, NotificationDelivery};
// Per-notification override; `.system()` and `.in_app_and_system()` are
// shorthands for `.delivery(NotificationDelivery::...)`.
Notification::info("Your download is ready.")
.title("Download complete")
.system()
// Or set a global default for all notifications
Theme::update(cx, |theme| {
theme.notification.delivery = NotificationDelivery::InAppAndSystem
});The notification’s title and message become the system notification’s title
and body; a notification with neither is not posted. Pushing again with the
same .id::<T>() replaces the previous system notification, and
window.remove_notification::<T>(cx) / window.clear_notifications(cx)
retract it. When the toast auto-hides, the system notification stays in the
notification center.
Clicking the system notification activates the application and its window,
closes the in-app toast (if any), and fires on_click with a default
ClickEvent. With NotificationDelivery::System no toast exists, so
on_close is never called.
gpui_kit::component::init registers the app-global
on_system_notification_response handler, so do not register your own after
it — gpui keeps only one. Notifications your application posts directly via
cx.show_system_notification are left untouched.
Platform requirements:
| Platform | Requirement | Retraction |
|---|---|---|
| macOS | Must run from a bundled .app in a trusted location (e.g. /Applications); silently disabled under plain cargo run. The first post triggers the system authorization prompt; a denial is remembered and later posts fail silently | Supported |
| Windows | Call cx.set_app_identity(identifier, name) early in startup | Supported |
| Linux | An XDG notification daemon must be present | Unsupported (ages out) |
#Batch Operation Complete
Processed: 150 items Success: 147 items Failed: 3 items
Notification::success(“Batch operation completed with some failures.”) .title(“Operation Results”) .content(|window, cx| { markdown(results_content).into_any_element() }) .autohide(false)
### Interactive Confirmation
```rust
struct SaveConfirmation;
Notification::new()
.id::<SaveConfirmation>()
.title("Unsaved Changes")
.message("You have unsaved changes. Save before leaving?")
.autohide(false)
.action(|_, cx| {
Button::new("save")
.primary()
.label("Save")
.on_click(cx.listener(|this, _, window, cx| {
// Perform save
println!("Saving changes...");
this.dismiss(window, cx);
}))
})
.on_click(cx.listener(|_, _, _, cx| {
println!("Save reminder clicked");
cx.notify();
}))