Popover
Shows content in a floating panel anchored to a trigger, portaled out of the page flow so it never disturbs the layout. Built on reka-ui's Popover primitives, so click, keyboard, and aria wiring come for free.
Basic
#trigger is rendered through reka's PopoverTrigger as-child — clicking it (or pressing Enter / Space while focused) toggles the panel. #default renders inside the standard shell. Both slots receive open and close helpers.
<script setup lang="ts">
import { Button, Popover } from 'frappe-ui'
</script>
<template>
<Popover>
<template #trigger>
<Button>Click me</Button>
</template>
<template #default="{ close }">
<div class="p-2 text-ink-gray-9">
<p>Popover content</p>
<Button class="mt-2" @click="close()">Close</Button>
</div>
</template>
</Popover>
</template>Side and alignment
Use side (top / right / bottom / left) and align (start / center / end) to position the panel. The panel flips and shifts automatically to stay within the viewport (collisionPadding controls the gap kept from the edge).
<script setup lang="ts">
import { Button, Popover } from 'frappe-ui'
</script>
<template>
<Popover side="right" align="center">
<template #trigger>
<Button>Open on the right</Button>
</template>
<template #default>
<div class="p-3 text-p-sm text-ink-gray-7">
Positioned with <code>side="right"</code> and
<code>align="center"</code>.
</div>
</template>
</Popover>
</template>Controlled open state
Bind v-model:open to drive the panel from outside, or read it to react to open / close. The component also exposes open() and close() methods via a template ref, and emits open / close events.
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Popover } from 'frappe-ui'
const open = ref(false)
</script>
<template>
<div class="flex items-center gap-2">
<Button @click="open = !open">Toggle from outside</Button>
<Popover v-model:open="open">
<template #trigger>
<Button>Trigger</Button>
</template>
<template #default>
<div class="p-3 text-p-sm text-ink-gray-7">
Open state is controlled via <code>v-model:open</code>.
</div>
</template>
</Popover>
</div>
</template>Non-dismissible
By default the popover closes on an outside click or focus. Set :dismissible="false" to keep it open until you close it explicitly — useful for panels with their own confirm / cancel actions.
<script setup lang="ts">
import { Button, Popover } from 'frappe-ui'
</script>
<template>
<Popover :dismissible="false">
<template #trigger>
<Button>Stays open</Button>
</template>
<template #default="{ close }">
<div class="p-3 text-ink-gray-9">
<p class="text-p-sm">
Clicking outside won't close this. Use the button instead.
</p>
<Button class="mt-2" @click="close()">Close</Button>
</div>
</template>
</Popover>
</template>Match trigger width
Set match-trigger-width to make the panel's min-width match the trigger. Handy for select-style menus where the panel should line up under a wide button.
<script setup lang="ts">
import { Button, Popover } from 'frappe-ui'
</script>
<template>
<Popover match-trigger-width>
<template #trigger>
<Button class="w-64">Wide trigger</Button>
</template>
<template #default>
<div class="p-3 text-p-sm text-ink-gray-7">
The content's min-width matches the trigger width.
</div>
</template>
</Popover>
</template>Bare
Set bare to drop the panel shell (background, border, shadow, rounding) so #default content can bring its own surface — useful for pickers and cards that are already styled. This replaces the deprecated #body slot.
<script setup lang="ts">
import { Button, Popover } from 'frappe-ui'
</script>
<template>
<Popover bare>
<template #trigger>
<Button>Custom surface</Button>
</template>
<template #default>
<!-- bare: no panel shell, the content brings its own surface -->
<div
class="max-h-72 w-64 overflow-y-auto rounded-2xl bg-surface-gray-7 p-4 text-p-sm text-ink-white shadow-2xl"
>
This panel has no default chrome — the popover renders the content
directly so it can use its own background, radius, and shadow.
</div>
</template>
</Popover>
</template>Arrow
Set arrow to render a small arrow that points back at the trigger. It's styled to match the panel surface.
<script setup lang="ts">
import { Button, Popover } from 'frappe-ui'
</script>
<template>
<Popover arrow>
<template #trigger>
<Button>With arrow</Button>
</template>
<template #default>
<div class="p-3 text-p-sm text-ink-gray-7">
An arrow points back at the trigger.
</div>
</template>
</Popover>
</template>Styling
The popover ships with its panel shell baked in (rounded-lg bg-surface-elevation-2 shadow-2xl ring-1 ring-black ring-opacity-5) — there are no class-injection props. Style it through the stable data-slot hooks instead:
| Hook | Element |
|---|---|
[data-slot="trigger"] | the trigger wrapper |
[data-slot="content"] | the portaled content (reka PopoverContent) |
[data-slot="content-body"] | the panel shell that owns the visuals |
Open / closed and motion phase are reflected as data-state="open" \| "closed" and data-motion="animated" \| "instant" for state-driven styling.
:where([data-slot='content-body']) {
/* your overrides */
}Motion
The panel animates by default — a scale-from-trigger entrance (180ms in / 140ms out). Opening via the keyboard skips the animation (data-motion="instant"), and prefers-reduced-motion is respected. No configuration is required.
Notes
- Use
#trigger+#defaultfor the standard click popover. Both slots get{ open, close }. - Reach for
v-model:openonly when an external control needs to drive the panel — clicking the trigger already toggles it. - For a panel that opens on hover (profile previews, link previews), use the dedicated
HoverCardcomponent instead of a popover.
Migrating from v0
The v0 API still works through v1.x — old props are mapped silently, and binding both the old and new prop logs a one-time dev warning. See the full mapping table in Migration from v0 → Popover / HoverCard.
API Reference
Show types
export type PopoverSide = 'top' | 'right' | 'bottom' | 'left'
export type PopoverAlign = 'start' | 'center' | 'end'
/** Legacy placement union (deprecated — use `side` + `align`). */
export type PopoverPlacement =
| 'top-start'
| 'top-end'
| 'bottom-start'
| 'bottom-end'
| 'right-start'
| 'right-end'
| 'left-start'
| 'left-end'
| 'top'
| 'bottom'
| 'right'
| 'left'
export interface PopoverProps {
/** Controls visibility (v-model:open). */
open?: boolean
/** Side of the trigger to render the content on. */
side?: PopoverSide
/** Alignment of the content along the chosen side. */
align?: PopoverAlign
/** Distance in px between the trigger and the content. */
offset?: number
/** Where to portal the content. */
portalTo?: string | HTMLElement
/** Padding in px kept from the viewport edge during collision handling. */
collisionPadding?: number
/** Whether the popover closes on outside interaction (click/focus). */
dismissible?: boolean
/** Whether the content's min-width matches the trigger width. */
matchTriggerWidth?: boolean
/**
* Render `#default` without the panel shell (no background, border, shadow,
* or rounding). The content brings its own surface. Mirrors Dialog's `bare`.
*/
bare?: boolean
/**
* Render a small arrow pointing at the trigger. Styled to match the panel
* surface.
*/
arrow?: boolean
// ---------------------------------------------------------------------------
// Deprecated props (kept working through v1.x — see Popover.vue back-compat).
// ---------------------------------------------------------------------------
/** @deprecated Use `open` / `v-model:open`. */
show?: boolean
/** @deprecated Use `<HoverCard>` for hover behavior. */
trigger?: 'click' | 'hover'
/** @deprecated Use `<HoverCard>` (`hoverDelay`, in seconds). */
hoverDelay?: number
/** @deprecated Use `<HoverCard>` (`leaveDelay`, in seconds). */
leaveDelay?: number
/** @deprecated Use `side` + `align`. */
placement?: PopoverPlacement
/** @deprecated Use the `data-slot` CSS hooks (no-op). */
popoverClass?: string | object | Array<string | object>
/** @deprecated Motion is now built in (no-op). */
transition?: 'default' | null
/** @deprecated Use `dismissible`. */
hideOnBlur?: boolean
/** @deprecated Use `matchTriggerWidth`. */
matchTargetWidth?: boolean
}
export interface PopoverEmits {
/** Fired when the open state changes (canonical). */
(event: 'update:open', value: boolean): void
/** Fired when the popover opens. */
(event: 'open'): void
/** Fired when the popover closes. */
(event: 'close'): void
/** @deprecated Use `update:open`. */
(event: 'update:show', value: boolean): void
}
/** Slot props passed to the new `#trigger` / `#default` slots. */
export interface PopoverSlotProps {
open: () => void
close: () => void
toggle: (flag?: boolean | Event) => void
isOpen: boolean
}
/**
* Slot props passed to the deprecated `#target` / `#body` / `#body-main`
* slots. Preserved verbatim so existing callers do not double-toggle.
*/
export interface PopoverLegacySlotProps {
togglePopover: () => void
updatePosition: () => void
open: () => void
close: () => void
toggle: (flag?: boolean | Event) => void
isOpen: boolean
}Controls visibility (v-model:open).
Side of the trigger to render the content on.
Alignment of the content along the chosen side.
Distance in px between the trigger and the content.
Where to portal the content.
Padding in px kept from the viewport edge during collision handling.
Whether the popover closes on outside interaction (click/focus).
Whether the content's min-width matches the trigger width.
Render `#default` without the panel shell (no background, border, shadow, or rounding). The content brings its own surface. Mirrors Dialog's `bare`.
Render a small arrow pointing at the trigger. Styled to match the panel surface.
Deprecated — Use `open` / `v-model:open`.
Deprecated — Use `<HoverCard>` for hover behavior.
Deprecated — Use `<HoverCard>` (`hoverDelay`, in seconds).
Deprecated — Use `<HoverCard>` (`leaveDelay`, in seconds).
Deprecated — Use `side` + `align`.
Deprecated — Use the `data-slot` CSS hooks (no-op).
Deprecated — Motion is now built in (no-op).
Deprecated — Use `dismissible`.
Deprecated — Use `matchTriggerWidth`.
| Slot | Payload |
|---|---|
trigger | PopoverSlotProps Trigger element. Rendered via reka PopoverTrigger as-child. |
default | PopoverSlotProps Popover content, rendered inside the shared panel shell. |
target | Deprecated — Use `#trigger`. Rendered via reka PopoverAnchor (manual wiring). |
body | Deprecated — Use `#default`. Full body override (no default chrome). |
body-main | Deprecated — Use `#default`. Inner content inside the default container. |
Trigger element. Rendered via reka PopoverTrigger as-child.
Popover content, rendered inside the shared panel shell.
Deprecated — Use `#trigger`. Rendered via reka PopoverAnchor (manual wiring).
Deprecated — Use `#default`. Full body override (no default chrome).
Deprecated — Use `#default`. Inner content inside the default container.
| Event | Payload |
|---|---|
open | [] Fired when the component opens. |
close | [] Fired when the component closes. |
update:open | [value: boolean] Fired when the open state changes. |
update:show | [value: boolean] Fired when the show changes. |
Fired when the component opens.
Fired when the component closes.
Fired when the open state changes.
Fired when the show changes.