Popover
A non-modal panel anchored to a button, for details or a small form. Escape or a click outside closes it.
Shared with the workspace
22 of 24 people have opened it. Sources stay attached.
Nine of twelve interviewees raised onboarding. From interviews-q3.pdf, pages 4 to 11.
Usage
use hydroxy_leptos::button::ButtonVariant;
use hydroxy_leptos::popover::*;
use leptos::prelude::*;
pub fn demo() -> AnyView {
let (open, set_open) = signal(Some(false));
view! {
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-5);">
// Uncontrolled. Opens below the trigger and focuses the first button inside.
<Popover>
<PopoverTrigger>"Sharing"</PopoverTrigger>
<PopoverContent>
<p style="margin: 0 0 4px; font: var(--text-ui);">"Shared with the workspace"</p>
<p style="margin: 0 0 12px; color: var(--text-2);">
"22 of 24 people have opened it. Sources stay attached."
</p>
<PopoverClose variant=ButtonVariant::Secondary>"Done"</PopoverClose>
</PopoverContent>
</Popover>
// Controlled, to the side. Escape closes and returns focus to the trigger.
<Popover
open=open
on_open_change=move |v| set_open.set(Some(v))
placement=Placement::new(Side::End, Align::Start)
>
<PopoverTrigger variant=ButtonVariant::Ghost>"Where this figure comes from"</PopoverTrigger>
<PopoverContent>
<p style="margin: 0; color: var(--text-2);">
"Nine of twelve interviewees raised onboarding. From interviews-q3.pdf, pages 4 to 11."
</p>
</PopoverContent>
</Popover>
<span>"Open: " {move || open.get().unwrap_or(false).to_string()}</span>
</div>
}
.into_any()
}use dioxus::prelude::*;
use hydroxy_dioxus::button::ButtonVariant;
use hydroxy_dioxus::popover::{Align, Placement, Popover, PopoverClose, PopoverContent, PopoverTrigger, Side};
#[component]
pub fn PopoverDemo() -> Element {
let mut open = use_signal(|| Some(false));
rsx! {
div { style: "display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-5);",
// Uncontrolled. Opens below the trigger and focuses the first button inside.
Popover {
PopoverTrigger { "Sharing" }
PopoverContent {
p { style: "margin: 0 0 4px; font: var(--text-ui);", "Shared with the workspace" }
p { style: "margin: 0 0 12px; color: var(--text-2);",
"22 of 24 people have opened it. Sources stay attached."
}
PopoverClose { variant: ButtonVariant::Secondary, "Done" }
}
}
// Controlled, to the side. Escape closes and returns focus to the trigger.
Popover {
open: open(),
on_open_change: move |v| open.set(Some(v)),
placement: Placement::new(Side::End, Align::Start),
PopoverTrigger { variant: ButtonVariant::Ghost, "Where this figure comes from" }
PopoverContent {
p { style: "margin: 0; color: var(--text-2);",
"Nine of twelve interviewees raised onboarding. From interviews-q3.pdf, pages 4 to 11."
}
}
}
span { "Open: {open().unwrap_or(false)}" }
}
}
}
Anatomy
| Component | data-part | Renders |
|---|---|---|
Popover | (none) | Provides the machine. Renders no element |
PopoverTrigger | trigger | button: aria-haspopup=dialog, aria-expanded, aria-controls, popovertarget, CSS anchor |
PopoverContent | content | role=dialog, popover=auto, tabindex=-1, anchored with flip, data-side |
PopoverClose | close | button that closes and returns focus to the trigger |
Keyboard
| Key | Does |
|---|---|
Enter / Space | On the trigger: opens and focuses the first focusable element inside |
Tab | Moves through the panel's content, then on through the page. The panel stays open |
Escape | Closes and returns focus to the trigger |
Props
| Prop | Type | Notes |
|---|---|---|
open | Option<bool> | Controlled open state. Pair with on_open_change |
default_open | bool | Initial open state when uncontrolled. Default false |
placement | Placement | Side and alignment. Default below, centred |
on_open_change | Callback<bool> | Called when the panel opens or closes |
variant | Option<ButtonVariant> | On PopoverTrigger: the button look, default secondary. On PopoverClose: opt into the button look |