Popover
Popover component for displaying floating content that appears when interacting with a trigger element. Supports multiple positioning options, custom content, different trigger methods, and automatic dismissal behaviors. Perfect for tooltips, menus, forms, and other contextual information.
Import
use gpui_component::popover::{Popover, PopoverContent};
Usage
Basic Popover
Popover::new("basic-popover")
.trigger(Button::new("trigger").label("Click me").outline())
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, _| {
div()
.p_4()
.child("Hello, this is a popover!")
.into_any()
})
})
})
Popover with Custom Positioning
use gpui::Corner;
Popover::new("positioned-popover")
.anchor(Corner::TopRight)
.trigger(Button::new("top-right").label("Top Right").outline())
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, _| {
div()
.p_4()
.w_64()
.child("This popover appears at the top right")
.into_any()
})
})
})
Form in Popover
let form = Form::new(window, cx);
Popover::new("form-popover")
.anchor(Corner::BottomLeft)
.trigger(Button::new("show-form").label("Open Form").outline())
.content(move |_, _| form.clone())
Right-Click Popover
use gpui::MouseButton;
Popover::new("context-menu")
.anchor(Corner::BottomRight)
.mouse_button(MouseButton::Right)
.trigger(Button::new("right-click").label("Right Click Me").outline())
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, cx| {
v_flex()
.gap_2()
.child("Context Menu")
.child(Divider::horizontal())
.child(
Button::new("action")
.label("Perform Action")
.on_click(cx.listener(|_, _, window, cx| {
window.push_notification("Action performed!", cx);
cx.emit(DismissEvent);
}))
)
.into_any()
})
.p_4()
})
})
Advanced Usage
Rich Content Popover
Popover::new("rich-content")
.trigger(Button::new("info").icon(IconName::Info).outline())
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, cx| {
v_flex()
.gap_4()
.max_w(px(400.))
.child(
h_flex()
.gap_2()
.items_center()
.child(Icon::new(IconName::Info).size_5())
.child("Information")
.text_lg()
.font_semibold()
)
.child(Divider::horizontal())
.child(
div()
.child("This is detailed information about the feature.")
.text_sm()
.text_color(cx.theme().muted_foreground)
)
.child(
h_flex()
.gap_2()
.justify_end()
.child(
Button::new("learn-more")
.label("Learn More")
.small()
.primary()
)
.child(
Button::new("close")
.label("Close")
.small()
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(DismissEvent);
}))
)
)
.into_any()
})
.p_4()
})
})
Unstyled Popover
// For custom styled popovers or when you want full control
Popover::new("custom-popover")
.no_style()
.trigger(Button::new("custom").label("Custom Style"))
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, cx| {
div()
.bg(cx.theme().accent)
.text_color(cx.theme().accent_foreground)
.p_6()
.rounded_xl()
.shadow_2xl()
.child("Fully custom styled popover")
.into_any()
})
})
})
Popover with Different Triggers
// Button trigger
Popover::new("button-trigger")
.trigger(Button::new("btn").label("Button Trigger"))
.content(content_fn)
// Custom element trigger
Popover::new("div-trigger")
.trigger(
div()
.p_2()
.bg(cx.theme().muted)
.rounded(px(4.))
.child("Click this div")
.cursor_pointer()
)
.content(content_fn)
// Icon trigger
Popover::new("icon-trigger")
.trigger(Icon::new(IconName::HelpCircle).size_5())
.content(content_fn)
Dismissible Popover with Actions
Popover::new("action-popover")
.trigger(Button::new("actions").label("Show Actions"))
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, cx| {
v_flex()
.gap_2()
.child("Choose an action:")
.child(Divider::horizontal())
.child(
v_flex()
.gap_1()
.child(
Button::new("copy")
.label("Copy")
.small()
.w_full()
.justify_start()
.on_click(cx.listener(|_, _, window, cx| {
window.push_notification("Copied!", cx);
cx.emit(DismissEvent);
}))
)
.child(
Button::new("paste")
.label("Paste")
.small()
.w_full()
.justify_start()
.on_click(cx.listener(|_, _, window, cx| {
window.push_notification("Pasted!", cx);
cx.emit(DismissEvent);
}))
)
.child(
Button::new("delete")
.label("Delete")
.small()
.w_full()
.justify_start()
.destructive()
.on_click(cx.listener(|_, _, window, cx| {
window.push_notification("Deleted!", cx);
cx.emit(DismissEvent);
}))
)
)
.into_any()
})
.p_2()
.min_w(px(120.))
})
})
Trigger Styling
// Full width trigger
Popover::new("full-width")
.trigger_style(StyleRefinement {
size: Size { width: Some(relative(1.0)), ..Default::default() },
..Default::default()
})
.trigger(Button::new("full").label("Full Width Button"))
.content(content_fn)
// Custom display
Popover::new("flex-trigger")
.trigger_style(StyleRefinement {
display: Some(Display::Flex),
..Default::default()
})
.trigger(Button::new("flex").label("Flex Button"))
.content(content_fn)
Positioning and Anchoring
Anchor Positions
use gpui::Corner;
// Top left (default)
.anchor(Corner::TopLeft) // Popover appears below trigger, aligned to left
// Top right
.anchor(Corner::TopRight) // Popover appears below trigger, aligned to right
// Bottom left
.anchor(Corner::BottomLeft) // Popover appears above trigger, aligned to left
// Bottom right
.anchor(Corner::BottomRight) // Popover appears above trigger, aligned to right
Positioning Behavior
The popover automatically:
- Snaps to window edges with 8px margin
- Adjusts position to stay within viewport
- Resolves anchor position relative to trigger bounds
- Handles collision detection with window boundaries
Trigger Methods
Mouse Button Configuration
use gpui::MouseButton;
// Left click (default)
.mouse_button(MouseButton::Left)
// Right click for context menus
.mouse_button(MouseButton::Right)
// Middle click
.mouse_button(MouseButton::Middle)
Selectable Triggers
The trigger element must implement the Selectable
trait. Most UI components like Button
, div
, etc. support this:
// Button automatically supports selection state
.trigger(Button::new("btn").label("Click me"))
// Custom elements with selection state
.trigger(my_custom_element.selected(is_selected))
Custom Content
PopoverContent Builder
The PopoverContent
provides a flexible way to create popover content:
PopoverContent::new(window, cx, |window, cx| {
// Return any element that implements IntoElement
v_flex()
.gap_3()
.child("Content goes here")
.child(Button::new("action").label("Action"))
.into_any()
})
Content Styling
PopoverContent can be styled using the Styled
trait:
PopoverContent::new(window, cx, content_fn)
.p_6() // Custom padding
.max_w(px(500.)) // Maximum width
.bg(cx.theme().card) // Custom background
Reusable Content Components
// Create reusable content components
struct InfoPopover {
title: String,
description: String,
}
impl InfoPopover {
fn render(&self, _: &mut Window, cx: &mut Context<PopoverContent>) -> AnyElement {
v_flex()
.gap_3()
.child(
div()
.text_lg()
.font_semibold()
.child(&self.title)
)
.child(
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child(&self.description)
)
.into_any()
}
}
// Use in popover
.content(|window, cx| {
let info = InfoPopover {
title: "Feature Info".to_string(),
description: "This feature helps you...".to_string(),
};
cx.new(|cx| {
PopoverContent::new(window, cx, move |window, cx| {
info.render(window, cx)
})
})
})
API Reference
Popover
Method | Description |
---|---|
new(id) | Create a new popover with element ID |
anchor(corner) | Set anchor position (TopLeft, TopRight, BottomLeft, BottomRight) |
mouse_button(button) | Set trigger mouse button (Left, Right, Middle) |
trigger(element) | Set the trigger element (must implement Selectable) |
trigger_style(style) | Apply custom styling to trigger container |
content(closure) | Set popover content builder function |
no_style() | Remove default popover styling |
PopoverContent
Method | Description |
---|---|
new(window, cx, content_fn) | Create popover content with builder function |
PopoverContent also implements Styled
for custom styling:
Method | Description |
---|---|
p_*() | Padding utilities |
m_*() | Margin utilities |
bg() | Background color |
text_color() | Text color |
rounded_*() | Border radius |
border_*() | Border styling |
shadow_*() | Shadow effects |
max_w() | Maximum width |
min_w() | Minimum width |
Default Styling
When not using no_style()
, popovers automatically apply:
.bg(cx.theme().popover) // Background color
.text_color(cx.theme().popover_foreground) // Text color
.border_1() // 1px border
.border_color(cx.theme().border) // Border color
.shadow_lg() // Large shadow
Dismissal Events
Popovers can be dismissed by:
- Clicking outside the popover (when styled)
- Pressing the Escape key
- Emitting a
DismissEvent
from content
// Emit DismissEvent to close popover
cx.emit(DismissEvent)
// Subscribe to dismissal in content
window.subscribe(&content_view, cx, |_, _: &DismissEvent, window, cx| {
// Handle popover dismissal
});
Examples
Tooltip-style Popover
Popover::new("tooltip")
.trigger(
div()
.child("Hover me")
.p_2()
.cursor_help()
)
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, _| {
div()
.p_2()
.text_xs()
.child("This is helpful information")
.into_any()
})
.max_w(px(200.))
})
})
Dropdown Menu
Popover::new("dropdown")
.anchor(Corner::BottomLeft)
.trigger(
Button::new("menu")
.label("Menu")
.icon_after(IconName::ChevronDown)
)
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, cx| {
v_flex()
.gap_1()
.child(menu_item("New File", IconName::Plus, cx))
.child(menu_item("Open File", IconName::FolderOpen, cx))
.child(Divider::horizontal())
.child(menu_item("Settings", IconName::Settings, cx))
.into_any()
})
.p_1()
.min_w(px(150.))
})
})
fn menu_item(label: &str, icon: IconName, cx: &Context<PopoverContent>) -> impl IntoElement {
Button::new(label.to_lowercase().replace(" ", "-"))
.icon(icon)
.label(label)
.small()
.ghost()
.w_full()
.justify_start()
.on_click(cx.listener(move |_, _, window, cx| {
window.push_notification(format!("{} clicked", label), cx);
cx.emit(DismissEvent);
}))
}
Confirmation Popover
Popover::new("confirm-delete")
.anchor(Corner::TopRight)
.trigger(
Button::new("delete")
.icon(IconName::Trash)
.destructive()
)
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, cx| {
v_flex()
.gap_3()
.child(
h_flex()
.gap_2()
.items_center()
.child(Icon::new(IconName::AlertTriangle).text_color(cx.theme().warning))
.child("Confirm Deletion")
.font_semibold()
)
.child(
div()
.text_sm()
.child("Are you sure you want to delete this item? This action cannot be undone.")
)
.child(
h_flex()
.gap_2()
.justify_end()
.child(
Button::new("cancel")
.label("Cancel")
.small()
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(DismissEvent);
}))
)
.child(
Button::new("confirm")
.label("Delete")
.small()
.destructive()
.on_click(cx.listener(|_, _, window, cx| {
window.push_notification("Item deleted", cx);
cx.emit(DismissEvent);
}))
)
)
.into_any()
})
.p_4()
.max_w(px(300.))
})
})
User Profile Popover
Popover::new("user-profile")
.anchor(Corner::BottomRight)
.trigger(
div()
.flex()
.items_center()
.gap_2()
.p_2()
.rounded(px(6.))
.hover(|this, cx| this.bg(cx.theme().muted))
.cursor_pointer()
.child(Avatar::new("user").name("John Doe").size(Size::Small))
.child("John Doe")
)
.content(|window, cx| {
cx.new(|cx| {
PopoverContent::new(window, cx, |_, cx| {
v_flex()
.gap_4()
.child(
h_flex()
.gap_3()
.items_center()
.child(Avatar::new("user").name("John Doe"))
.child(
v_flex()
.gap_1()
.child("John Doe")
.child(
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child("john.doe@example.com")
)
)
)
.child(Divider::horizontal())
.child(
v_flex()
.gap_1()
.child(profile_menu_item("Profile", IconName::User, cx))
.child(profile_menu_item("Settings", IconName::Settings, cx))
.child(profile_menu_item("Help", IconName::HelpCircle, cx))
.child(Divider::horizontal())
.child(profile_menu_item("Sign Out", IconName::LogOut, cx))
)
.into_any()
})
.p_3()
.w(px(240.))
})
})
Accessibility
The Popover component follows accessibility best practices:
Keyboard Navigation
- Escape key: Closes the popover when pressed
- Focus management: Focus moves to popover content when opened
- Focus return: Focus returns to trigger element when closed
- Tab navigation: Proper tab order within popover content
Screen Reader Support
- ARIA attributes: Proper labeling for assistive technology
- Content announcement: Popover content is announced when opened
- Relationship indicators: Clear relationship between trigger and content
Mouse and Touch
- Click outside: Dismiss popover when clicking outside (when styled)
- Touch support: Works with touch devices and touch events
- Mouse button configuration: Support for different mouse buttons
Best Practices
- Focusable content: Ensure popover content is keyboard accessible
- Clear triggers: Make trigger elements clearly interactive
- Dismissal options: Always provide a way to close the popover
- Content structure: Use semantic HTML in popover content
- Testing: Test with keyboard navigation and screen readers
Accessibility Guidelines
// Good: Clear, focusable trigger
Button::new("info")
.icon(IconName::Info)
.label("More Information") // Clear label
.tooltip("Click for details") // Additional context
// Good: Keyboard dismissal in content
Button::new("close")
.label("Close")
.on_click(cx.listener(|_, _, _, cx| {
cx.emit(DismissEvent); // Explicit close action
}))
// Good: Semantic content structure
PopoverContent::new(window, cx, |_, _| {
div()
.role("dialog")
.aria_label("Additional information")
.child(content)
.into_any()
})
Focus Management
// The popover automatically manages focus:
// 1. Focus moves to popover when opened
// 2. Focus is trapped within popover content
// 3. Focus returns to trigger when closed
// For custom focus handling:
PopoverContent::new(window, cx, |_, cx| {
div()
.track_focus(&focus_handle) // Explicit focus tracking
.child(content)
.into_any()
})
Performance Considerations
Efficient Content Creation
// Good: Lazy content creation
.content(|window, cx| {
// Content is only created when popover opens
cx.new(|cx| expensive_content_creation(window, cx))
})
// Avoid: Pre-creating content
let content = expensive_content_creation(); // Created immediately
.content(move |_, _| content.clone())
Memory Management
// The popover automatically manages content lifecycle:
// - Content is created when popover opens
// - Content is destroyed when popover closes
// - No memory leaks from unclosed popovers
// For complex content, consider cleanup:
window.subscribe(&content_view, cx, |_, _: &DismissEvent, _, _| {
// Cleanup resources when popover closes
});
Styling Performance
// Good: Use theme colors for consistency
.bg(cx.theme().popover)
.text_color(cx.theme().popover_foreground)
// Good: Minimal custom styling
PopoverContent::new(window, cx, content_fn)
.p_4() // Simple padding
// Avoid: Complex nested styling in hot paths