Skip to content
PorabuildHeroGPUI component library
v0.14.0 · Apache-2.0

Overlays

Popover

A floating panel anchored to a trigger.

Rust
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.

Usage

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>.

Rust
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).

With Arrow

`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>.

Rust
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).

Interactive Content

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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).

Placement

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
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).

Render Function

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>.

Rust
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).

Custom Styles

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>.

Rust
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.

overlay_min_h
fn overlay_min_h(mut frame: gpui::Div, open: bool, height: f32) -> gpui::Div {
    if open {
        frame = frame.min_h(px(height));
    }
    frame
}
set_popover_open
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);
    }
}
MONO_FONT
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

PopoverPopoverArrow

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

Popover styling
MethodValuesDescription
placement(PopoverPlacement)PlacementThe 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.
Rust
// 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

Popover builders
BuilderValuesDefaultDescription
new(trigger)PartialAnyElement—Compound trigger and content children; takes the trigger positionally and panel children through ParentElement.
is_open(bool)true | false—Controlled open state.
default_open(bool)true | falsefalseInitial uncontrolled open state.
on_open_change(callback)Fn(isOpen: bool) -> ()—Reports trigger, Escape, outside-press and explicit close changes.
placement(PopoverPlacement)PlacementPopoverPlacement::BottomThe 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(Pixels)number8Distance between trigger and panel.
should_flip(bool)true | falsetrueChanges to the opposite orientation when the preferred side would overflow and the opposite side fits better.
new() + ParentElement childrenPartialAnyElement—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
Popover builders not ported
HeroUI v3Why it is not ported
Popover.Content.classNameAdditional classes have no GPUI analogue.
Popover.Content.renderElement replacement is not available.
Popover.Dialog.classNameAdditional dialog classes.
Popover.Trigger.classNameAdditional trigger classes.
Popover.Arrow.classNameAdditional arrow classes.
Popover.Arrow.renderArrow replacement is not available.

Parts

Popover parts
PartDescription
PopoverControlled or uncontrolled owner of open state, focus scope and dismissal lifecycle.
PopoverPartialInteractive wrapper around the positional trigger child; not separately replaceable or styled.
PopoverPartialPlacement-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.
PopoverArrowPartialComposed 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.
PopoverPartialDialog focus scope and 16px content inset are built into the monolithic panel.
PopoverPartialThe title builder draws the medium-weight heading, but Heading is not an independently composable part.

States

Popover states
StateBuilderDescription
EnteringPartialoverlay_scope Open150ms Smooth fade and zoom from 90% with the 4px translation from the resolved physical placement; GPUI still has no transform-origin primitive.
Exitingoverlay_scope ExitingRemains mounted for the 100ms Smooth fade and zoom to 95%.
PlacementPartialPopoverPositionerEight placements align to the trigger and shouldFlip resolves the fitting orientation; placement attributes and the wider union are absent.
Focus visiblePartialtrigger child focus treatmentKeyboard focus and its visible ring remain owned by the caller-provided trigger control.