Dialog
A focused confirmation step, with dismissal and trigger focus restoration.

use blinc_cn::{ButtonVariant, button, dialog};
use blinc_core::reactive::{Signal, computed};
use blinc_layout::{binding::IntoReactive, prelude::*};
pub fn dialog_demo(confirmed: Signal<bool>, disabled: Signal<bool>) -> Div {
div()
.class("demo-stack")
.class("demo-stack--center")
.child(
button("Open dialog")
.variant(ButtonVariant::Outline)
.disabled(disabled)
.on_click(move |_| {
dialog()
.title("Publish this draft?")
.description("Your changes are ready to share.")
.confirm_text("Publish")
.cancel_text("Keep editing")
.on_confirm(move || confirmed.set(true))
.show();
})
.build_component()
.id("demo-action"),
)
.child(
bound_text(computed(move |graph| {
if graph.get(confirmed).unwrap_or(false) {
"Published.".to_string()
} else {
"Draft saved.".to_string()
}
}))
.class("demo-caption"),
)
}
fn bound_text(value: impl IntoReactive<String>) -> Text {
Text::bound(value.into_reactive())
}
@layer components {
.cn-dialog {
font-family: var(--font-sans);
}
.cn-dialog {
background: var(--cn-dialog-bg, var(--surface-elevated));
border: 1px solid var(--cn-dialog-border, var(--border));
border-radius: var(--radius-xl);
color: var(--text-primary);
box-shadow: var(--cn-dialog-shadow, var(--shadow-xl));
padding: var(--space-6);
gap: var(--space-4);
}
.cn-dialog-title {
color: var(--text-primary);
}
.cn-dialog-description {
color: var(--text-secondary);
}
@keyframes cn-rise-in {
from {
opacity: 0;
transform: translateY(-4px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes cn-rise-out {
from {
opacity: 1;
transform: translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateY(-4px) scale(0.96);
}
}
.cn-dialog-motion[data-motion="enter"] {
animation: cn-rise-in var(--duration-normal) var(--ease-sheet);
}
.cn-dialog-motion[data-motion="exit"] {
animation: cn-rise-out var(--duration-fast) var(--ease-in) forwards;
}
}
Usage and properties
Build and show the dialog from the trigger handler. The returned OverlayHandle supports explicit close. Dialog parts live in the overlay root, so selectors beneath .catalog-demo do not reach them.
Use the installation guide to add blinc_cn and its component stylesheet. Create the fixture bindings once, and return the demo from your UI builder. Values marked Bound accept Signal/Computed sources where the API supports them; structural options configure the component at construction.
| Property | Type | Update model | This example |
|---|---|---|---|
title / description | String | Bound | Publish this draft? |
confirm_text / cancel_text | String | Bound | Publish / Keep editing |
on_confirm | Fn() | Callback | Set the published Signal |
on_cancel / on_close | Fn() | Callback | Optional dismissal feedback |
show() | OverlayHandle | Lifecycle | Called from the trigger |
Keyboard and interaction
Tab stays within the active dialog. Escape and backdrop click dismiss it. Closing restores focus to the trigger. Confirmation and cancellation callbacks take no arguments.
Customizing it
Add application CSS after the default component layer. These selectors are the restyling surface for this component:
.cn-dialog.cn-dialog-title.cn-dialog-description.cn-dialog-motion
Theme and component variables: --surface-elevated, --text-primary, --text-secondary, --border, --radius-lg, --focus-ring.
Built-in themes
The Live theme control installs Blinc's own theme bundles, including their colours, focus rings, corner shapes and motion tokens.
pub fn theme_bundle_for(name: &str) -> blinc_theme::ThemeBundle {
match name {
"restrained" => blinc_theme::RestrainedTheme::bundle(),
"expressive" => blinc_theme::ExpressiveTheme::bundle(),
_ => blinc_theme::HybridTheme::bundle(),
}
.with_css(blinc_cn::cn_styles::CN_STYLES)
}Download the default widget and component stylesheets. Sample assets and licenses are available in the asset guide.
Documentation
From the canonical component guide.
Dialogs ask for a decision. Sheets and drawers place content beside the main
view. Install cn_bundle() as described in Getting Started
to load their styles and motion.
The examples use these imports. ctx is the WindowedContext supplied to
the application builder:
use blinc::cn::prelude::*;
use blinc::layout::prelude::{div, text};
Open a Dialog
Configure a builder and call .show() from an event handler:
button("Open dialog").on_click(|_| {
dialog()
.title("Continue?")
.description("Review the details before continuing.")
.content(|| div().child(text("Your current selection will be used.")))
.confirm_text("Continue")
.on_confirm(|| {
toast_success("Confirmed").show();
})
.show();
})
.show() returns an OverlayHandle. Keep it when the application needs to
call handle.close(). The default footer has Cancel and Confirm buttons,
which close the dialog. Escape and a backdrop click also dismiss it.
on_confirm and on_cancel report their respective buttons.
on_close reports dismissal by any route, including Escape and the backdrop.
These callbacks take no arguments.
Confirmation Dialog
Use confirm_destructive(true) for a destructive action:
let deleted = ctx.use_signal(false);
button("Delete draft")
.variant(ButtonVariant::Destructive)
.on_click(move |_| {
dialog()
.title("Delete draft?")
.description("This removes the draft from this session.")
.confirm_text("Delete")
.cancel_text("Keep draft")
.confirm_destructive(true)
.on_confirm(move || deleted.set(true))
.show();
})
alert_dialog() is an acknowledgement dialog: it hides Cancel. It is not a
separate set of trigger, content or action components.
button("Show notice").on_click(|_| {
alert_dialog()
.title("Import complete")
.description("Your files are ready to use.")
.confirm_text("OK")
.size(DialogSize::Small)
.show();
})
Form Dialog
Create editor data once and clone its handle into the deferred body. Input changes can update a Signal that the confirmation callback reads:
use blinc::layout::widgets::text_input::text_input_data;
let name_data = text_input_data();
let name = ctx.use_signal(String::new());
button("Edit profile").on_click(move |_| {
let name_data = name_data.clone();
dialog()
.title("Edit profile")
.description("Update the name shown on your profile.")
.content(move || {
div().child(
input(&name_data)
.label("Name")
.placeholder("Your name")
.on_change(move |value| name.set(value.to_string())),
)
})
.confirm_text("Save")
.on_confirm(move || {
toast_success("Profile saved")
.description(name.get())
.show();
})
.show();
})
The default footer suits this form. .footer(|| div()...) replaces the
entire footer, including its automatic Cancel and Confirm buttons; custom
actions must arrange dismissal themselves.
Text That Changes
Titles, descriptions, confirm_text and cancel_text accept strings,
Signals or Computeds. Bind them before showing the dialog:
let remaining = ctx.use_signal("3 files".to_string());
button("Review files").on_click(move |_| {
dialog()
.title("Selected files")
.description(remaining)
.content(move || {
div().child(button("Remove one from selection").on_click(move |_| {
remaining.set("2 files".to_string());
}))
})
.confirm_text("Done")
.show();
})
The description changes in place while the dialog remains open.
Sheet
A sheet can enter from any edge. The default is the right edge:
button("Open settings").on_click(|_| {
sheet()
.side(SheetSide::Right)
.size(SheetSize::Medium)
.title("Settings")
.description("Preferences for this workspace.")
.content(|| div().child(text("Settings content")))
.footer(|| div().child(muted("Changes are saved automatically.")))
.show();
})
sheet_left(), sheet_right(), sheet_top() and sheet_bottom() select the
edge directly. SheetSize is Small, Medium, Large or Full.
.show_close(false) hides the close button; .on_close(|| ...) observes
dismissal. The title and description accept reactive values.
Sheet and Drawer close controls are standard ghost icon buttons. Their existing
.cn-sheet-close and .cn-drawer-close classes remain available for styling.
Drawer
A drawer is a left or right navigation panel, with the left as the default:
button("Open navigation").on_click(|_| {
drawer_left()
.size(DrawerSize::Medium)
.title("Workspace")
.child(|| div().child(text("Projects and recent activity")))
.footer(|| div().child(muted("Signed in as Jane")))
.show();
})
DrawerSide has Left and Right; DrawerSize has Narrow, Medium and
Wide. Use a bottom sheet for a panel entering from below. .header(|| ...)
replaces the title header. Each .child(|| ...) adds a body section.
When opened with the keyboard, a dialog focuses its first enabled control.
Tab and Shift+Tab stay inside the topmost dialog or nested popup. Closing
restores focus to the opening control if it still exists and is enabled.
Pointer focus uses the same target without a keyboard outline; keyboard
navigation uses the shared :focus-visible rules.
Styling and Motion
The panel classes are .cn-dialog, .cn-sheet and .cn-drawer. Dialog text
uses .cn-dialog-title and .cn-dialog-description; sheets use
.cn-sheet-title and .cn-sheet-description; drawers use .cn-drawer-title.
Add a panel class with .class("...") and override it in application CSS.
The overlay wrapper carries data-motion="enter" or "exit" and remains
mounted while its exit animation plays. Child transforms stay inside that
motion, so rotated icons slide with the panel. The backdrop is .overlay-backdrop.
| Overlay | Motion wrapper | Default motion |
|---|---|---|
| Dialog | .cn-dialog-motion | cn-rise-in, cn-rise-out |
| Sheet | .cn-sheet-motion-{left,right,top,bottom} | cn-slide-from-{side}, cn-slide-to-{side} |
| Drawer | .cn-drawer-motion-{left,right} | cn-slide-from-{side}, cn-slide-to-{side} |
Timing comes from theme tokens. Change it through rules:
.cn-dialog-motion[data-motion="enter"] {
animation: cn-rise-in 400ms var(--ease-sheet);
}
.cn-sheet-motion-right[data-motion="exit"] {
animation: cn-slide-to-right var(--duration-fast) var(--ease-in) forwards;
}
Builder Reference
| Builder | Content | Actions | Presentation |
|---|---|---|---|
dialog() | title, description, content | confirm_text, cancel_text, on_confirm, on_cancel, on_close, hide_cancel, footer | size, confirm_destructive, class, id |
alert_dialog() | title, description | confirm_text, on_confirm | size |
sheet() | title, description, content, footer | on_close, show_close | side, size |
drawer() | title, header, child, child_builder, footer | on_close, show_close | side, size, class, id |
All four open with .show(). Their builders have no .open(signal) or
.on_open_change(...) API. Dialog sizes are Small (400px maximum),
Medium (500px), Large (600px) and Full (800px).
Platforms and version
The component API is available in Blinc 0.6.0. The Live example runs in a dedicated worker and requires a compatible GPU surface. Preview and Code remain available when Live cannot start. The canvas has a descriptive label; a full screen-reader DOM mirror is not provided. The gallery integration is based on revision e2e55c24e078.
Recordings and live demos use the default Hybrid theme with bundled Noto Sans. The recording uses a 560 × 320 logical viewport at twice that resolution. Live follows its container and actual display density. Source base revision · Exact build source · Artifact versions and checksums · Font license.