Overlays & Navigation Widgets
Overlay Z-Ordering
Overlays are rendered above the main content at the root level:
- Modal - lowest (backdrop covers entire screen)
- Popover - middle
- Toast - highest (always visible)
Modal
Centered dialog overlay. Portals to root level regardless of declaration location.
| Prop | Type | Description |
|---|---|---|
title | impl Into<String> | Constructor - dialog title |
child | Element | Dialog content |
scope | OverlayScope | RootPortal (default) or Local |
on_close | Callback<()> | Close callback (Esc/backdrop click) |
width | Length | Dialog width |
height | Length | Dialog height |
backdrop_style | Style | Backdrop overlay style |
frame_style | Style | Dialog container style |
border_style | BorderStyle | Dialog border |
padding | impl Into<Padding> | Dialog inner padding |
title_style | Style | Title style |
if ctx.state.show_confirm {
Modal::new("Confirm Delete")
.child(
VStack::new()
.gap(1)
.child(Text::new("This action cannot be undone."))
.child(
HStack::new().gap(1)
.child(Button::new("Cancel")
.on_click(ctx.link().callback(|_| Msg::CancelDelete)))
.child(Button::new("Delete")
.style(Style::new().fg(Color::White).bg(Color::Red))
.on_click(ctx.link().callback(|_| Msg::ConfirmDelete)))
)
)
.on_close(ctx.link().callback(|_| Msg::CancelDelete))
.into()
}Toast Notifications
Toasts use ctx.toast() - no view tree setup required.
App configuration:
App::new()
.toast_placement(ToastPlacement::BottomEnd) // default
.toast_gap(1)
.mount(Root)
.run()Showing toasts:
fn update(&mut self, msg: Msg, ctx: &mut Context<Self>) -> Update {
match msg {
Msg::SaveSuccess => {
ctx.toast().push(Toast::new("Saved successfully!"));
Update::full()
}
Msg::SaveError(e) => {
ctx.toast().push(
Toast::new(format!("Save failed: {e}"))
.title("Error")
.border(true)
);
Update::full()
}
Msg::ShowCustom => {
let id = ctx.toast().push(
Toast::new("Custom message")
.duration(10.0)
.border(true)
);
ctx.state.toast_id = Some(id); // Store to dismiss later
Update::full()
}
Msg::DismissToast => {
if let Some(id) = ctx.state.toast_id.take() {
ctx.toast().dismiss(id);
}
Update::none()
}
}
}ToastHandle methods:
| Method | Description |
|---|---|
.push(Toast) | Show toast, returns OverlayId |
.dismiss(id) | Dismiss a specific toast |
.clear() | Clear all toasts |
ToastPlacement: TopStart, TopCenter, TopEnd, BottomStart, BottomCenter, BottomEnd (default).
Toast props:
| Prop | Type | Description |
|---|---|---|
message | impl Into<String> | Constructor - toast text |
duration | f32 | Auto-dismiss seconds (0 = permanent) |
title | String | Optional title |
title_prefix | String | Title prefix symbol |
title_suffix | String | Title suffix |
title_alignment | Align | Title alignment |
title_style | Style | Title style |
message_style | Style | Message style |
frame_style | Style | Container style |
border | bool | Show border |
border_style | BorderStyle | Border style |
padding | impl Into<Padding> | Padding |
width | Length | Width |
max_width | Length | Maximum width |
wrap | bool | Wrap long messages |
decoration / decorations | FrameDecoration | Edge decorations |
Toasts are suppressed in inline mode to avoid terminal history corruption.
Popover
Floating content panel triggered by an element.
| Prop | Type | Description |
|---|---|---|
trigger | Element | The trigger element |
content | Element | Popover content |
open | bool | Controlled open state |
scope | OverlayScope | RootPortal (default) or Local |
on_close | Callback<()> | Close callback |
placement | PopoverPlacement | Top, Bottom, Left, Right + variants |
offset | u16 | Distance from trigger |
clamp | bool | Keep within screen bounds |
auto_flip | bool | Flip placement when out of bounds |
match_trigger_width | bool | Match popover width to trigger |
anchor | PopoverAnchor | Alignment relative to trigger |
Popover renders through the root overlay pipeline by default, so it appears above normal in-tree content. Use .scope(OverlayScope::Local) when it should stay inside parent stacking order, such as an autocomplete attached to content that can be covered by an inline sidebar layer.
Tooltip
Help text on hover or focus.
| Prop | Type | Description |
|---|---|---|
text | impl Into<String> | Constructor - tooltip text |
child | Element | The element to add tooltip to |
open | bool | Controlled open state |
auto | bool | Auto-show on hover/focus |
text_style | Style | Tooltip text style |
container_style | Style | Tooltip container style |
border | bool | Show border |
border_style | BorderStyle | Border style |
padding | impl Into<Padding> | Inner padding |
placement | PopoverPlacement | Tooltip placement |
offset | u16 | Distance from element |
clamp | bool | Keep within screen bounds |
auto_flip | bool | Flip when out of bounds |
Tooltip::new("This button saves your work")
.auto(true)
.placement(PopoverPlacement::Top)
.child(
Button::new("Save")
.on_click(ctx.link().callback(|_| Msg::Save))
.into()
)Accordion
Collapsible content sections.
| Prop | Type | Description |
|---|---|---|
exclusive | bool | Only one section open at a time |
gap | u16 | Gap between sections |
padding | impl Into<Padding> | Outer padding |
border | bool | Section border |
border_style | BorderStyle | Section border style |
style | Style | Container style |
header_style | Style | Header idle style |
header_hover_style | Style | Header hover style |
header_focus_style | Style | Header focus style |
header_padding | impl Into<Padding> | Header padding |
content_padding | impl Into<Padding> | Content area padding |
content_border | bool | Content area border |
content_style | Style | Content area style |
expanded_icon | char | Expanded section icon |
collapsed_icon | char | Collapsed section icon |
disabled_style | Style | Style when disabled |
focusable | bool | Whether headers participate in focus traversal |
width | Length | Width |
height | Length | Height |
on_toggle | Callback<AccordionEvent> | Section toggle event |
Accordion::new()
.exclusive(true)
.item(AccordionItem::new(
"Section 1",
Text::new("Content for section 1").into()
))
.item(AccordionItem::new(
"Section 2",
Text::new("Content for section 2").into()
))SearchPalette
Fuzzy search widget powered by nucleo. Composes an Input and List into a filterable, keyboard-navigable search panel.
SearchPalette is not an overlay by itself - wrap it in Modal for the classic command-palette experience, or embed it inline in a Frame, sidebar, or any other container.
CommandPalette
CommandPalette is a composite overlay widget that reads commands from ctx.command_registry() and renders them through SearchPalette<CommandId> inside a Modal.
Use ctx.register_command(...) inside a component to register component-scoped commands, or ctx.command_registry().register(...) for app-wide commands.
| Prop | Type | Description |
|---|---|---|
on_close | Callback<()> | Fired when the modal closes or a command executes |
show_disabled | bool | Include disabled commands in results (muted and non-activating) |
title | impl Into<RichText> | Modal title |
width | Length | Modal width |
height | Length | Modal height |
scope | OverlayScope | RootPortal (default) or Local |
backdrop_style | Style | Backdrop overlay style |
frame_style | Style | Modal frame style |
border | bool | Show modal border |
border_style | BorderStyle | Modal border style |
padding | impl Into<Padding> | Modal content padding |
title_style | Style | Title style |
title_alignment | Align | Title alignment |
fn init(&mut self, ctx: &mut Context<Self>) -> Option<Command> {
let link = ctx.link().clone();
ctx.register_command(
CommandEntry::builder("app.toggle-wrap")
.label("Toggle word wrap")
.description("Enable or disable editor wrapping")
.category("Application")
.keybinding("p")
.handler(Callback::new(move |_| link.send(Msg::ToggleWrap)))
.build(),
);
None
}
fn view(&self, ctx: &Context<Self>) -> Element {
if ctx.state.show_palette {
CommandPalette::new()
.title("Commands")
.show_disabled(true)
.on_close(ctx.link().callback(|_| Msg::ClosePalette))
.into()
} else {
Element::empty()
}
}Command ids are open-ended (CommandId) and can be grouped with optional categories and right-aligned keybinding hints.
Items can be provided flat via .items() or grouped via .entries() using SearchEntry::item(...), SearchEntry::header(...), and SearchEntry::spacer().
Headers and spacers are display-only rows (not searchable/selectable). Group rendering rules:
- With an empty query, all item rows are shown and headers/spacers render in entry order.
- With a non-empty query, grouped chrome is hidden and matches render as a flat ranked list.
Item text is typically built from SearchItem::new(label, value) and optional .description("..."). By default, description renders inline as label - description and uses description_style.
You can also add hidden aliases with SearchItem::alias(...) or SearchItem::aliases(...). Aliases are searched and scored like the label, but they are never rendered, which makes them useful for abbreviations, legacy names, or alternate command titles.
You can also mark rows active with .active(true) on SearchItem or SearchEntry::item(...).
Core props
| Prop | Type | Description |
|---|---|---|
items | impl IntoIterator<Item = SearchItem<T>> | Flat searchable items (clears entries) |
entries | impl IntoIterator<Item = SearchEntry<T>> | Grouped entries via item/header/spacer rows |
sync_match_limit | usize | Max item count that still matches synchronously (default: 100) |
sync_selection | bool | Keep on_select synced with the current visible row |
initial_query | impl Into<Arc<str>> | Pre-populate search field |
initial_selected_item_index | Option<usize> | Start selection on this items index when it appears in results (else first row) |
placeholder | impl Into<Arc<str>> | Input placeholder (default: "Search...") |
width | Length | Requested palette width (default: Flex(1)) |
height | Length | Requested palette height (default: Flex(1)) |
max_width | Length | Maximum palette width constraint |
max_height | Length | Maximum palette height constraint |
Callbacks
| Prop | Type | Description |
|---|---|---|
on_query_change | Callback<Arc<str>> | Fired when the query text changes |
on_select | Callback<SearchEvent<T>> | Fired when selection moves; with sync_selection(true) also fires for initial/result-driven selection |
on_activate | Callback<SearchEvent<T>> | Fired on Enter or double-click |
Input forwarding
| Prop | Type | Description |
|---|---|---|
input_prefix | impl Into<Arc<str>> | Prefix before query text (default: " ") |
input_suffix | impl Into<Arc<str>> | Suffix after query text (default: "{matches}/{total}") |
input_border | bool | Show input border |
input_divider | bool | Render divider below input (uncontrolled mode, default: true) |
input_divider_style | Style | Divider style below input |
input_divider_join_frame | bool | Join divider with surrounding frame border (default: true) |
input_caret_shape | CaretShape | Input caret shape (Block, Bar, Underline) |
input_caret_color | Color | Input caret color (OSC 12 cursor color, terminal support required) |
input_border_style | BorderStyle | Input border style |
input_padding | impl Into<Padding> | Input padding |
input_style | Style | Input base style |
input_hover_style | Style | Input hover style |
input_focus_style | Style | Input focus style |
input_placeholder_style | Style | Placeholder style |
input_focus_placeholder_style | Style | Placeholder style when focused |
input_prefix_style | Style | Prefix style |
input_focus_prefix_style | Style | Prefix style when focused |
input_suffix_style | Style | Suffix style |
input_focus_suffix_style | Style | Suffix style when focused |
List forwarding
| Prop | Type | Description |
|---|---|---|
list_border | bool | Show list border |
list_border_style | BorderStyle | List border style |
list_padding | impl Into<Padding> | List padding |
list_style | Style | List base style |
list_hover_style | Style | List hover style |
list_selection_style | Style | Selected item style |
list_selection_symbol | impl Into<Arc<str>> | Selection indicator (default: "> ") |
list_selection_symbol_style | Style | Selection indicator style |
list_unselected_symbol | impl Into<Arc<str>> | Non-selected item indent |
list_selection_full_width | bool | Extend selection to full width |
list_item_hover_style | Style | Individual item hover style |
list_active_style | Style | Active item style |
list_active_symbol | impl Into<Arc<str>> | Active item symbol |
list_active_symbol_style | Style | Active symbol style |
list_item_horizontal_padding | impl Into<Padding> | Normal row padding (left/right used) |
list_header_horizontal_padding | impl Into<Padding> | Header row padding (left/right used) |
list_focusable | bool | Allow list keyboard focus (default: true) |
list_scrollbar | bool | Show scrollbar |
list_scrollbar_config | ScrollbarConfig | Full scrollbar configuration (variant, gap, thumb, thumb styles) |
empty_text | impl Into<Arc<str>> | Empty state text (default: "No matches") |
empty_text_style | Style | Empty state text style |
list_item_horizontal_paddingandlist_header_horizontal_paddingacceptPadding, but onlyleftandrightare applied byList.
Item rendering
| Prop | Type | Description |
|---|---|---|
item_style | Style | Item label base style |
description_style | Style | Item description style |
description_placement | DescriptionPlacement | Description placement: Inline, Right, Above, Below |
description_selection | bool | Whether selection highlight applies to description text |
description_overflow | DescriptionOverflow | Description overflow policy: Truncate or Wrap (Wrap applies to Above/Below) |
match_style | Style | Matched character style |
show_scores | bool | Show numeric match scores |
score_gradient | ColorGradient | Gradient for score coloring |
score_range | (u64, u64) | Explicit score range for gradient |
render_item | SearchRenderer<T> | Custom item renderer (Arc<dyn Fn>) |
description_selection(false)has no effect withDescriptionPlacement::InlineandDescriptionPlacement::Right, because selection/hover styling applies to the whole primary row in those modes.
description_selectionalso controls description hover styling whenlist_item_hover_styleis set.
description_overflow(DescriptionOverflow::Wrap)affects onlyDescriptionPlacement::AboveandDescriptionPlacement::Below; inline and right placement keep single-row truncation behavior.
Matching config
| Prop | Type | Description |
|---|---|---|
case_matching | CaseMatching | Case sensitivity (default: Smart) |
normalization | Normalization | Unicode normalization (default: Smart) |
Matching uses synchronous updates for lists up to
sync_match_limititems and off-threadnucleosearches for larger lists; items do not needSend/Sync.
Standalone ranking - rank_search_palette_indices(&[SearchItem<T>], query) returns each item’s index in the source slice ordered like the palette’s fuzzy results (smart case matching and normalization). Use it when another widget owns the query/focus but you need the same ordering for keyboard selection.
As a modal overlay - wrap in Modal and set on_close/size there:
if ctx.state.show_palette {
let palette = SearchPalette::<Arc<str>>::new()
.entries(vec![
SearchEntry::header("Sources"),
SearchEntry::item("src/lib.rs", Arc::from("src/lib.rs"))
.description("Crate root"),
SearchEntry::header("Examples"),
SearchEntry::item("examples/demo.rs", Arc::from("examples/demo.rs")),
])
.list_scrollbar(true)
.list_selection_full_width(true)
.list_item_hover_style(Style::new().bg(Color::DarkGray))
.on_activate(ctx.link().callback(Msg::Activated));
Modal::new("Open File")
.child(palette)
.width(Length::Px(60))
.height(Length::Px(20))
.border_style(BorderStyle::Rounded)
.padding(0)
.on_close(ctx.link().callback(|_| Msg::ClosePalette))
}Inline - embed directly without Modal:
Frame::new()
.title("Search")
.border(true)
.child(SearchPalette::<Arc<str>>::new().items(my_items))ContextMenu
A popup menu backed by Popover + List. Typically triggered by right-click or a keyboard shortcut.
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | impl IntoElement | Constructor | The element that owns the menu |
items | impl IntoIterator<Item = impl Into<ListItem>> | [] | Menu items |
open | bool | false | Controlled open state |
on_select | Callback<usize> | - | Fires with selected item index |
on_close | Callback<()> | - | Fires when menu should close |
anchor | Option<(u16, u16)> | None | Absolute anchor position (content coordinates) |
placement | PopoverPlacement | BelowStart | Menu placement relative to trigger |
offset | impl Into<PopoverOffset> | 0 | Distance from trigger |
clamp | bool | true | Keep within screen bounds |
auto_flip | bool | true | Flip placement when out of bounds |
width | Length | Px(20) | Menu width |
height | Length | Auto | Menu height |
border | bool | true | Show border |
border_style | BorderStyle | Plain | Border style |
padding | impl Into<Padding> | default | Inner padding |
style | Style | default | Base style |
selection_style | Style | default | Selected item style |
item_hover_style | Style | - | Hovered item style |
selection_symbol | Option<impl Into<Arc<str>>> | "> " | Selection indicator |
selection_symbol_style | Style | - | Selection indicator style |
scrollbar | bool | false | Show scrollbar |
scrollbar_config | ScrollbarConfig | default | Scrollbar configuration |
ContextMenu::new(
Button::new("Options").on_click(ctx.link().callback(|_| Msg::ToggleMenu))
)
.items(vec!["Cut", "Copy", "Paste", "Delete"])
.open(ctx.state.menu_open)
.on_select(ctx.link().callback(Msg::MenuAction))
.on_close(ctx.link().callback(|_| Msg::CloseMenu))
.selection_style(Style::new().bg(Color::DarkGray))