Context Menu
Show actions at the pointer or keyboard-focused target.

use blinc_cn as cn;
use blinc_core::reactive::{Signal, computed};
use blinc_layout::{binding::IntoReactive, prelude::*};
pub fn context_menu_demo(choice: Signal<String>, confirmed: Signal<bool>) -> Div {
let favorite = confirmed;
div()
.class("demo-stack")
.id("demo-action")
.on_context_menu(move |event| {
use blinc_icons::{icons, to_svg};
let (label, icon) = if favorite.get() {
("Remove from favorites", icons::STAR_OFF)
} else {
("Add to favorites", icons::STAR)
};
cn::context_menu()
.at(event.mouse_x, event.mouse_y)
.item_with_icon("Duplicate", to_svg(icons::COPY, 16.0), move || {
choice.set("duplicate".into())
})
.item_with_icon("Rename", to_svg(icons::PENCIL, 16.0), move || {
choice.set("rename".into())
})
.item_with_icon("Share", to_svg(icons::SHARE_2, 16.0), move || {
choice.set("share".into())
})
.separator()
.item_with_icon(label, to_svg(icon, 16.0), move || {
let next = !favorite.get();
favorite.set(next);
choice.set(if next { "favorite" } else { "unfavorite" }.into());
})
.show();
})
.child(
cn::card()
.w_full()
.child(
div()
.class("demo-profile")
.child(
svg(blinc_icons::to_svg(blinc_icons::icons::FOLDER, 28.0))
.size(28.0, 28.0)
.class("demo-icons"),
)
.child(
div()
.class("demo-profile-copy")
.child(text("Website redesign").class("demo-label"))
.child(
bound_text(computed(move |g| {
if g.get(favorite).unwrap_or(false) {
"Team workspace · Favorite"
} else {
"Team workspace"
}
.into()
}))
.class("demo-caption"),
),
),
)
.child(
bound_text(computed(move |g| {
match g.get(choice).as_deref() {
Some("duplicate") => "Duplicate selected",
Some("rename") => "Rename selected",
Some("share") => "Share selected",
Some("favorite") => "Added to favorites",
Some("unfavorite") => "Removed from favorites",
_ => "Right-click for project actions",
}
.into()
}))
.class("demo-caption"),
),
)
}
fn bound_text(value: impl IntoReactive<String>) -> Text {
Text::bound(value.into_reactive())
}
@layer components {
.cn-context-menu,
.cn-context-menu-item {
font-family: var(--font-sans);
}
.cn-menu-item {
color: var(--text-primary);
font-size: var(--text-sm);
cursor: pointer;
}
.cn-menu-item:hover:not(:disabled),
.cn-menu-item:focus-visible:not(:disabled) {
background: var(--accent-subtle);
}
.cn-menu-item:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.cn-context-menu {
animation: cn-context-menu-enter var(--duration-fast) var(--ease-state);
transform-origin: top left;
}
@keyframes cn-context-menu-enter {
from {
opacity: 0;
transform: scale(0.96) translateY(-2px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.cn-context-menu:has(.cn-context-menu-item--has-submenu:hover) {
background: var(--surface-elevated);
animation: none;
}
.cn-context-menu-item {
padding: var(--space-1-5) var(--space-3);
}
}
Usage and properties
Right-click the Website redesign project for icon-labelled actions. Add or remove it from favorites; the project details and next menu update in place.
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 |
|---|---|---|---|
items | Vec<ContextMenuItem> | Construction | Project actions |
on_context_menu | EventContext | Callback | Opens at the pointer position |
disabled | bool or IntoReactive<bool> | Bound | values.disabled |
Keyboard and interaction
While the menu is open Up and Down wrap through enabled rows, Home and End jump to the ends, Enter or Space activates, Right opens a submenu and Left returns. Escape closes and restores focus; Tab closes and moves on.
Customizing it
Add application CSS after the default component layer. These selectors are the restyling surface for this component:
.cn-context-menu.cn-menu-item.cn-context-menu-item.cn-context-menu-item--has-submenu
Theme and component variables: --primary, --text-primary, --text-secondary, --surface, --border, --radius-default, --duration-fast, --ease-state.
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.
Open a context menu at the pointer's window coordinates:
let action = ctx.use_signal(String::new());
div().w(240.0).h(160.0)
.child(text("Right-click here"))
.on_context_menu(move |event| {
context_menu()
.at(event.mouse_x, event.mouse_y)
.item("Cut", move || action.set("cut".to_string()))
.item("Copy", move || action.set("copy".to_string()))
.separator()
.item("Delete", move || action.set("delete".to_string()))
.show();
})
on_context_menu makes the target a Tab stop. Shift+F10 or the Context Menu
key opens at the focused target; right-click opens at the pointer. The menu shares
the dropdown's arrow, activation and dismissal keys. The returned overlay
handle can close the menu programmatically.
Add SVG icons with item_with_icon; the menu aligns each icon with its label:
context_menu().at(x, y)
.item_with_icon("Duplicate", blinc::icons::to_svg(blinc::icons::icons::COPY, 16.0), || {})
.item_with_icon("Share", blinc::icons::to_svg(blinc::icons::icons::SHARE_2, 16.0), || {})
.show();
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.