Dialog, Sheet and Drawer

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:

#![allow(unused)]
fn main() {
use blinc::cn::prelude::*;
use blinc::layout::prelude::{div, text};
}

Open a Dialog

Configure a builder and call .show() from an event handler:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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.

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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:

#![allow(unused)]
fn main() {
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.

OverlayMotion wrapperDefault motion
Dialog.cn-dialog-motioncn-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

BuilderContentActionsPresentation
dialog()title, description, contentconfirm_text, cancel_text, on_confirm, on_cancel, on_close, hide_cancel, footersize, confirm_destructive, class, id
alert_dialog()title, descriptionconfirm_text, on_confirmsize
sheet()title, description, content, footeron_close, show_closeside, size
drawer()title, header, child, child_builder, footeron_close, show_closeside, 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).