Popover
A floating panel anchored to a trigger.
use herogpui::components::popover::{Popover, PopoverArrow, PopoverPlacement};Popover on docs.rsSource at v0.14.0
Usage
Showing Usage, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.
Panel text and headings use a 20px line height independently of surrounding text. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Button, Popover, PopoverArrow, PopoverPlacement, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
use std::collections::HashMap;
let is_open = self.popover_open;
let usage_slot = gpui::div().relative().flex().flex_col().items_start();
overlay_min_h(usage_slot, is_open, 160.)
.child(
Popover::new(
gpui::div()
.pr(px(96.))
.child(
Button::new("popover-trigger")
.label("Open popover")
.variant(Variant::Secondary),
),
)
.is_open(is_open)
.title("Quick note")
.placement(PopoverPlacement::Bottom)
.padding(px(18.))
.radius(px(4.))
.child(gpui::div().child("Popovers are anchored to their trigger."))
.on_open_change(cx.listener(|this, open: &bool, _, cx| {
set_popover_open(
&mut this.popover_open,
&mut this.demo_flags,
"po-usage",
*open,
);
cx.notify();
})),
)Also calls overlay_min_h, set_popover_open (included when you copy).
`PopoverArrow::new()` composes the arrow part: the built-in 12px curve follows the resolved side when the panel flips and preserves the configured offset. A custom child element takes the resolved position but no rotation: the placement transform is not reproducible on an arbitrary element (only `svg()` transforms at construction). Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{ActiveTheme, Button, Popover, PopoverArrow, PopoverPlacement, Variant, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
use std::collections::HashMap;
let po_arrow_open = self.demo_overlay("po-arrow");
let po_arrow_custom_open = self.demo_overlay("po-arrow-custom");
gpui::div()
.relative()
.flex()
.flex_wrap()
.items_start()
.gap(px(24.))
.pl(px(48.))
.child(
overlay_min_h(
gpui::div()
.relative()
.flex()
.flex_col()
.items_start(),
po_arrow_open,
160.,
)
.child(
Popover::new(
Button::new("popover-arrow-trigger")
.label("Offset by 12px")
.variant(Variant::Secondary),
)
.id("popover-arrow")
.is_open(po_arrow_open)
.on_open_change(cx.listener(|this, open: &bool, _, cx| {
set_popover_open(
&mut this.popover_open,
&mut this.demo_flags,
"po-arrow",
*open,
);
cx.notify();
}))
.offset(px(12.))
.title("Anchored")
.child(gpui::div().child("Twelve pixels clear of the trigger."))
.child(PopoverArrow::new()),
)
)
.child(
overlay_min_h(
gpui::div()
.relative()
.flex()
.flex_col()
.items_start(),
po_arrow_custom_open,
160.,
)
.child(
Popover::new(
Button::new("popover-arrow-custom-trigger")
.label("Custom arrow")
.variant(Variant::Secondary),
)
.id("popover-arrow-custom")
.is_open(po_arrow_custom_open)
.on_open_change(cx.listener(|this, open: &bool, _, cx| {
set_popover_open(
&mut this.popover_open,
&mut this.demo_flags,
"po-arrow-custom",
*open,
);
cx.notify();
}))
.offset(px(12.))
.title("Custom arrow")
.child(gpui::div().child("A caller-drawn element, not the curve."))
.child(
PopoverArrow::new().child(
gpui::svg()
.size(px(12.))
.path(icons::TOOLTIP_ARROW)
.text_color(cx.colors().accent.foreground),
),
),
)
)Also calls overlay_min_h, set_popover_open (included when you copy).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Avatar, Button, Popover, PopoverArrow, PopoverPlacement, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
use std::collections::HashMap;
let po_following = self.demo_flag("po-following", false);
let po_interactive_open = self.demo_overlay("po-interactive");
{
let interactive_slot = gpui::div()
.relative()
.flex()
.flex_col()
.items_start();
overlay_min_h(interactive_slot, po_interactive_open, 220.)
.child(
Popover::new(
gpui::div()
.flex()
.items_center()
.gap(px(8.))
.pr(px(96.))
.child(Avatar::new("popover-avatar").name("Sarah Johnson").size(Size::Sm))
.child(gpui::div().child("Sarah Johnson")),
)
.id("popover-interactive")
.is_open(po_interactive_open)
.on_open_change(cx.listener(|this, open: &bool, _, cx| {
set_popover_open(
&mut this.popover_open,
&mut this.demo_flags,
"po-interactive",
*open,
);
cx.notify();
}))
.title("Sarah Johnson")
.child(
gpui::div()
.flex()
.flex_col()
.gap(px(10.))
.child(gpui::div().child("Design lead, Berlin"))
.child(
Button::new("popover-follow")
.label(if po_following {
"Following"
} else {
"Follow"
})
.size(Size::Sm)
.variant(if po_following {
Variant::Secondary
} else {
Variant::Primary
})
.on_press(cx.listener(move |this, _, _, cx| {
this.set_demo_flag("po-following", !po_following);
cx.notify();
})),
),
),
)
.into_any_element()
}Also calls overlay_min_h, set_popover_open (included when you copy).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{Button, Popover, PopoverArrow, PopoverPlacement, Size, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
use std::collections::HashMap;
gpui::div()
.relative()
.flex()
.flex_wrap()
.gap(px(24.))
.children(
[
("po-pl-top", "Top", PopoverPlacement::Top),
("po-pl-bottom", "Bottom", PopoverPlacement::Bottom),
("po-pl-left", "Left", PopoverPlacement::Left),
("po-pl-right", "Right", PopoverPlacement::Right),
]
.into_iter()
.map(|(id, label, placement)| {
let open = self.demo_overlay(id);
let mut placement_slot = gpui::div()
.relative()
.flex()
.flex_col()
.items_start();
if open && matches!(placement, PopoverPlacement::Top) {
placement_slot = placement_slot.pt(px(96.));
}
if open
&& matches!(
placement,
PopoverPlacement::Top | PopoverPlacement::Left
)
{
placement_slot = placement_slot.pl(px(104.));
}
overlay_min_h(placement_slot, open, 260.)
.child(
Popover::new(
Button::new(format!("{id}-trigger"))
.label(label)
.variant(Variant::Secondary)
.size(Size::Sm),
)
.id(id)
.is_open(open)
.on_open_change(cx.listener(
move |this, open: &bool, _, cx| {
set_popover_open(
&mut this.popover_open,
&mut this.demo_flags,
id,
*open,
);
cx.notify();
},
))
.placement(placement)
.title(label)
.child(gpui::div().child("Anchored to its trigger.")),
)
.into_any_element()
}),
)Also calls overlay_min_h, set_popover_open (included when you copy).
The pinned Render Function replaces the Popover content's element with a callback. This GPUI Popover has no content or state render callback, so the controlled panel records that limitation instead of faking an API. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{ActiveTheme, Button, Popover, PopoverArrow, PopoverPlacement, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
use std::collections::HashMap;
let po_render_open = self.demo_overlay("po-render-function");
let colors = cx.colors().clone();
overlay_min_h(
gpui::div()
.relative()
.flex()
.flex_col()
.items_start(),
po_render_open,
160.,
)
.child(
Popover::new(
gpui::div()
.pr(px(96.))
.child(
Button::new("popover-render-function-trigger")
.label("Click me")
.variant(Variant::Secondary),
),
)
.id("popover-render-function")
.is_open(po_render_open)
.on_open_change(cx.listener(
|this, open: &bool, _, cx| {
set_popover_open(
&mut this.popover_open,
&mut this.demo_flags,
"po-render-function",
*open,
);
cx.notify();
},
))
.child(
gpui::div()
.child(
gpui::div()
.font_weight(gpui::FontWeight::MEDIUM)
.child("Popover Title"),
)
.child(
gpui::div()
.mt(px(8.))
.text_size(px(14.))
.text_color(colors.muted)
.child(
"This is the popover content. You can put any content here.",
),
),
),
)Also calls overlay_min_h, set_popover_open (included when you copy).
The pinned styling is expressed here with public GPUI builders: `w`, `overflow_hidden`, `rounded`, `border_color`, `bg`, `shadow`, spacing, and `font_family`, using the active theme tokens. GPUI 0.2.2 has no class, ring, gradient, or backdrop-blur hooks, so the styling belongs to the composed content element. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
use herogpui::prelude::{ActiveTheme, Button, Popover, PopoverArrow, PopoverPlacement, Variant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;
use std::collections::HashMap;
let po_custom_styles_open = self.demo_overlay("po-custom-styles");
let colors = cx.colors().clone();
let custom_border = colors.border.alpha(0.8);
let custom_surface = colors.surface.background.alpha(0.9);
let custom_tint = colors
.default
.color
.alpha(if cx.is_dark_theme() { 0.08 } else { 0.06 });
let custom_shadow = cx.layout().overlay_shadow.clone();
overlay_min_h(
gpui::div()
.relative()
.flex()
.flex_col()
.items_start(),
po_custom_styles_open,
160.,
)
.child(
Popover::new(
gpui::div()
.pr(px(96.))
.child(
Button::new("popover-custom-styles-trigger")
.label("Details")
.variant(Variant::Secondary),
),
)
.id("popover-custom-styles")
.is_open(po_custom_styles_open)
.on_open_change(cx.listener(
|this, open: &bool, _, cx| {
set_popover_open(
&mut this.popover_open,
&mut this.demo_flags,
"po-custom-styles",
*open,
);
cx.notify();
},
))
.child(
gpui::div()
.relative()
.w(px(224.))
.overflow_hidden()
.rounded(px(12.))
.border_1()
.border_color(custom_border)
.bg(custom_surface)
.shadow(custom_shadow)
.p(px(16.))
.child(
gpui::div()
.absolute()
.top_0()
.left_0()
.right_0()
.h(px(48.))
.bg(custom_tint),
)
.child(
gpui::div()
.relative()
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(colors.foreground)
.child("Keyboard shortcuts"),
)
.child(
gpui::div()
.relative()
.mt(px(12.))
.flex()
.flex_col()
.gap(px(8.))
.text_size(px(14.))
.child(
gpui::div()
.flex()
.justify_between()
.gap(px(16.))
.child(
gpui::div()
.text_color(colors.muted)
.child("Save"),
)
.child(
gpui::div()
.font_family(MONO_FONT)
.text_color(colors.foreground)
.child("⌘ S"),
),
)
.child(
gpui::div()
.flex()
.justify_between()
.gap(px(16.))
.child(
gpui::div()
.text_color(colors.muted)
.child("Search"),
)
.child(
gpui::div()
.font_family(MONO_FONT)
.text_color(colors.foreground)
.child("⌘ K"),
),
),
),
),
)Also calls overlay_min_h, set_popover_open, MONO_FONT (included when you copy).
Helpers used by these examples
Plain functions over the public API, defined once here and copied with each example that calls them.
fn overlay_min_h(mut frame: gpui::Div, open: bool, height: f32) -> gpui::Div {
if open {
frame = frame.min_h(px(height));
}
frame
}fn set_popover_open(
usage: &mut bool,
flags: &mut HashMap<&'static str, bool>,
key: &'static str,
open: bool,
) {
if key == "po-usage" {
*usage = open;
} else {
flags.insert(key, open);
}
}const MONO_FONT: &str = if cfg!(target_arch = "wasm32") {
// The web shell bundles JetBrains Mono; the literal family "monospace"
// resolves to nothing there and panics text resolution.
"JetBrains Mono"
} else if cfg!(target_os = "macos") {
"Menlo"
} else {
"Consolas"
};Anatomy
Rust types
Popover is assembled from these builders. The API reference lists each one.
Customization
Shared appearance belongs on ThemeBuilder::components and .recipe("name"). Typed builders change how Popover looks at one call site. Every builder also carries sx for placement, width and flex: it takes GPUI's own styling methods and refines them over the component's root after the theme, so an instance override wins. Behaviour builders live in the API reference below.
Styling
| Method | Values | Description |
|---|---|---|
placement(PopoverPlacement) | Placement | The full 22-value union renders: physical and aligned spellings plus the logical start/end aliases, which share their left/right side's pixels in this LTR-only port. |
// The one slot for caller-owned low-level styling: GPUI's
// styling methods, refined over the root element last.
.sx(|el| {
el.bg(gpui::rgba(0xffa500ff)) // background
.text_color(gpui::rgba(0x000000ff)) // text
.w(gpui::px(13.)) // size
.h(gpui::px(12.))
})API reference
Against HeroUI v3.2.6: 8 entries ported, 13 partial, 6 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.
Builders
| Builder | Values | Default | Description |
|---|---|---|---|
new(Partial | AnyElement | — | Compound trigger and content children; takes the trigger positionally and panel children through ParentElement. |
is_open( | true | | — | Controlled open state. |
default_open( | true | | false | Initial uncontrolled open state. |
on_open_change( | Fn( | — | Reports trigger, Escape, outside-press and explicit close changes. |
placement( | Placement | PopoverPlacement:: | The full 22-value union renders: physical and aligned spellings plus the logical start/end aliases, which share their left/right side's pixels in this LTR-only port. |
offset( | number | 8 | Distance between trigger and panel. |
should_flip( | true | | true | Changes to the opposite orientation when the preferred side would overflow and the opposite side fits better. |
new(Partial | AnyElement | — | The built-in 12px curve renders by default and a custom element composes through ParentElement at the resolved arrow position. Upstream stamps the single child with data-slot=popover-overlay-arrow so the.popover placement CSS rotates it too; gpui-pre 0.3.3 transforms only svg elements, so a custom child takes the position without the rotation. |
Not ported6 HeroUI entries not ported
| HeroUI v3 | Why it is not ported |
|---|---|
Popover.Content.className | Additional classes have no GPUI analogue. |
Popover.Content.render | Element replacement is not available. |
Popover.Dialog.className | Additional dialog classes. |
Popover.Trigger.className | Additional trigger classes. |
Popover.Arrow.className | Additional arrow classes. |
Popover.Arrow.render | Arrow replacement is not available. |
Parts
| Part | Description |
|---|---|
Popover | Controlled or uncontrolled owner of open state, focus scope and dismissal lifecycle. |
PopoverPartial | Interactive wrapper around the positional trigger child; not separately replaceable or styled. |
PopoverPartial | Placement-aware surface with true orientation flipping and pinned motion; gpui-pre 0.3.3 cannot nest deferred portal draws, so the panel remains in its ancestor clipping context. |
PopoverArrowPartial | Composed part drawing the built-in 12px curved arrow centred on the trigger and rotated for the resolved side, or a caller-supplied child element at the resolved position. Upstream rotates the single custom child through its data-slot placement CSS; gpui-pre 0.3.3's svg-only transformation cannot reproduce that for an arbitrary element. |
PopoverPartial | Dialog focus scope and 16px content inset are built into the monolithic panel. |
PopoverPartial | The title builder draws the medium-weight heading, but Heading is not an independently composable part. |
States
| State | Builder | Description |
|---|---|---|
EnteringPartial | overlay_scope Open | 150ms Smooth fade and zoom from 90% with the 4px translation from the resolved physical placement; GPUI still has no transform-origin primitive. |
Exiting | overlay_scope Exiting | Remains mounted for the 100ms Smooth fade and zoom to 95%. |
PlacementPartial | PopoverPositioner | Eight placements align to the trigger and shouldFlip resolves the fitting orientation; placement attributes and the wider union are absent. |
Focus visiblePartial | trigger child focus treatment | Keyboard focus and its visible ring remain owned by the caller-provided trigger control. |