Component reference
Popover
A floating panel anchored to a trigger, in the top layer.
Preview
Child controls and modal closing
render() {
let dialog: HTMLDialogElement | undefined;
const childOpen = signal(false);
const mounted = signal(true);
const parentKeys = signal(0);
const childKeys = signal(0);
let closeFromCallback = false;
return (
<Stack gap="normal">
<div>
<Button onClick={() => dialog?.showModal()}>Open popover dialog</Button>
</div>
<Dialog
id="escape-dialog"
labelledBy="escape-title"
ref={(node: HTMLDialogElement) => {
dialog = node;
}}
>
<DialogPanel
header={
<DialogHeader
title="Local editing and returning"
titleId="escape-title"
actions={
<Button variant="quiet" onClick={() => dialog?.close()}>
Close dialog
</Button>
}
/>
}
>
<Stack gap="normal">
<p>
Input controls handle their own Escape first; if it is unhandled, the current
popover closes.
</p>
<Popover
id="escape-parent"
onKeyDown={(event) => {
if (event.key === "Escape") parentKeys.value++;
}}
trigger={(bindings) => <Button {...bindings}>Open parent popover</Button>}
>
<Stack gap="normal">
<Field label="Local draft">
<Input aria-label="Local draft" />
</Field>
{mapSignal(mounted, (shown) =>
shown ? (
<Popover
id="escape-child"
open={childOpen}
onOpenChange={(next) => {
childOpen.value = next;
}}
onKeyDown={(event) => {
if (event.key === "Escape") {
childKeys.value++;
if (closeFromCallback) childOpen.value = false;
}
}}
trigger={(bindings) => <Button {...bindings}>Open child popover</Button>}
>
<Stack gap="normal">
<Input aria-label="Child popover draft" />
<Button
onClick={() => {
closeFromCallback = true;
}}
>
Close child popover via callback
</Button>
<Button
onClick={() => {
childOpen.value = false;
}}
>
Close child popover
</Button>
<Button
onClick={() => {
mounted.value = false;
}}
>
Remove child popover
</Button>
</Stack>
</Popover>
) : (
<Button
onClick={() => {
childOpen.value = false;
mounted.value = true;
}}
>
Restore child popover
</Button>
),
)}
</Stack>
</Popover>
<output id="escape-callbacks">
Parent {parentKeys} · Child {childKeys}
</output>
</Stack>
</DialogPanel>
</Dialog>
</Stack>
);
}Installation
bun add meowtail
Usage
import { Popover } from "meowtail";Examples
The other stories that show this component.
In-place editing and returning
Expands content next to the current action; it can nest, open and close under control, and Escape returns to the previous layer.
render() {
const controlled = signal(false);
const retained = signal(false);
const redirected = signal(false);
let external: HTMLElement | undefined;
const paneTitle = signal("Choose settings");
let inPane = false;
return (
<div className="workshop-stack workshop-constrained">
<header className="workshop-composition-header">
<h2>Settings kept at hand</h2>
<p>Change how things are displayed, then return to what you were reading.</p>
</header>
<div className="workshop-actions">
<Popover
id="display-popover"
role="dialog"
labelledBy="display-title"
trigger={(bindings) => (
<Button variant="outline" {...bindings}>
Display settings
</Button>
)}
>
<div className="workshop-section">
<header className="workshop-composition-header">
<h2 id="display-title">Display settings</h2>
<p>Affects only the current preview.</p>
</header>
<Field label="Display name" htmlFor="popover-name">
<Input id="popover-name" value="Work view" />
</Field>
<Popover
id="nested-popover"
role="dialog"
labelledBy="nested-title"
align="end"
trigger={(bindings) => (
<Button variant="quiet" {...bindings}>
More settings
</Button>
)}
>
<div className="workshop-section">
<h3 id="nested-title" className="workshop-section-title">
Reading width
</h3>
<Note>Keep space for the body and let supporting actions step aside.</Note>
<Input aria-label="Width note" placeholder="Write down your preference…" />
</div>
</Popover>
</div>
</Popover>
<Popover
id="controlled-popover"
open={controlled}
onOpenChange={(next) => {
controlled.value = next;
}}
role="dialog"
labelledBy="controlled-title"
trigger={(bindings) => (
<Button variant="quiet" {...bindings}>
Open controlled panel
</Button>
)}
>
<div className="workshop-section">
<h3 id="controlled-title" className="workshop-section-title">
Open and close managed by the host
</h3>
<Note>Focus returns to the trigger after closing.</Note>
<Button
onClick={() => {
controlled.value = false;
}}
>
Close controlled panel
</Button>
</div>
</Popover>
<Button
variant="quiet"
ref={(element: HTMLElement) => {
external = element;
}}
>
External action
</Button>
<Popover
id="retained-popover"
open={retained}
onOpenChange={(next) => {
if (next) retained.value = true;
}}
role="dialog"
labelledBy="retained-title"
trigger={(bindings) => (
<Button variant="quiet" {...bindings}>
Keep controlled panel
</Button>
)}
>
<h3 id="retained-title">Host keeps the current panel</h3>
<Input aria-label="Retained panel draft" value="Waiting for the host" />
</Popover>
<Popover
id="redirected-popover"
open={redirected}
onOpenChange={(next) => {
redirected.value = next;
if (!next) external?.focus();
}}
role="dialog"
labelledBy="redirected-title"
trigger={(bindings) => (
<Button variant="quiet" {...bindings}>
Continue external action after closing
</Button>
)}
>
<h3 id="redirected-title">Host chooses the next focus</h3>
<p>Continue to the external action after closing.</p>
</Popover>
<Popover
id="pane-popover"
role="dialog"
labelledBy="pane-title"
onKeyDown={(event) => {
if (event.key === "Escape" && inPane) {
event.preventDefault();
inPane = false;
paneTitle.value = "Choose settings";
}
}}
onOpenChange={() => {
inPane = false;
paneTitle.value = "Choose settings";
}}
trigger={(bindings) => (
<Button variant="quiet" {...bindings}>
Layered settings
</Button>
)}
>
<div className="workshop-section">
<h3 id="pane-title">{paneTitle}</h3>
<Button
onClick={() => {
inPane = true;
paneTitle.value = "Settings details";
}}
>
Open details
</Button>
</div>
</Popover>
</div>
</div>
);
}API reference
Props
| Prop | Type | Default |
|---|---|---|
idRequired | string | – |
triggerId | string | `${id}-trigger` |
triggerRequired | (bindings: PopoverTriggerBindings) => Child | – |
open | MaybeSignal<boolean> | – |
defaultOpen | boolean | false |
onOpenChange | (open: boolean) => void | – |
matchTriggerWidth | boolean | false |
align | "start" | "end" | "start" |
placement | "top" | "bottom" | "right" | "bottom" |
anchorRect | () => DOMRect | – |
anchorElementElement whose layout defines a custom rect; observed by the same autoUpdate. | () => HTMLElement | null | – |
role | string | – |
labelledBy | string | – |
onKeyDownPrevent default on Escape to leave an inner pane before dismissing the layer. | (event: KeyboardEvent) => void | – |
Also takes className, children and the attributes and events of its root element.
DOM parts
rootpanel
Registry
The component is registered as popover in the meowtail component registry.