<script setup lang="ts">
import { PhBell } from "@phosphor-icons/vue";
import { Button } from "@dicehub/phi/components/button";
import { Popover } from "@dicehub/phi/components/popover";
</script>
<template>
<Popover>
<Popover.Trigger as-child>
<Button shape="square" :icon="PhBell" aria-label="Notifications" />
</Popover.Trigger>
<Popover.Content>
<Popover.Title>Notifications</Popover.Title>
<Popover.Description>You are all caught up. Good job!</Popover.Description>
</Popover.Content>
</Popover>
</template>Installation
Barrel
import { Popover } from "@dicehub/phi";Granular
import { Popover } from "@dicehub/phi/components/popover";Usage
<script setup lang="ts">
import { Button } from "@dicehub/phi/components/button";
import { Popover } from "@dicehub/phi/components/popover";
</script>
<template>
<Popover>
<Popover.Trigger as-child>
<Button>Open</Button>
</Popover.Trigger>
<Popover.Content>
<Popover.Title>Popover Title</Popover.Title>
<Popover.Description>Popover content goes here.</Popover.Description>
</Popover.Content>
</Popover>
</template>Popover vs Tooltip
While popovers can be triggered on hover (using openOnHover), they serve a different purpose than tooltips. Understanding when to use each is important for accessibility and user experience.
| Tooltip | Popover | |
|---|---|---|
| Purpose | Short, non-interactive text labels for identification | Rich, interactive content containers |
| Content | Plain text only | Any content: links, buttons, forms, images |
| Trigger | Hover or focus | Click (default) or hover |
| ARIA Role | role="tooltip" | aria-haspopup |
| Keyboard | Not focusable | Focus moves inside, traps when open |
Use a Tooltip when you need to label an icon button or provide a brief explanation. Use a Popover when users need to interact with the content inside, such as clicking links, filling out forms, or dismissing with a button.
Examples
Basic Popover
A simple popover with a title and description.
<script setup lang="ts">
import { Button } from "@dicehub/phi/components/button";
import { Popover } from "@dicehub/phi/components/popover";
</script>
<template>
<Popover>
<Popover.Trigger as-child>
<Button>Open Popover</Button>
</Popover.Trigger>
<Popover.Content>
<Popover.Title>Popover Title</Popover.Title>
<Popover.Description>
This is a basic popover with a title and description.
</Popover.Description>
</Popover.Content>
</Popover>
</template>With Close Button
Use Popover.Close to dismiss interactive content from inside the popover.
<script setup lang="ts">
import { Button } from "@dicehub/phi/components/button";
import { Popover } from "@dicehub/phi/components/popover";
</script>
<template>
<Popover>
<Popover.Trigger as-child>
<Button>Open Settings</Button>
</Popover.Trigger>
<Popover.Content>
<Popover.Title>Settings</Popover.Title>
<Popover.Description>Configure your preferences below.</Popover.Description>
<div class="mt-3">
<Popover.Close as-child>
<Button variant="secondary" size="sm">Close</Button>
</Popover.Close>
</div>
</Popover.Content>
</Popover>
</template>Positioning
Use the side prop to control where the popover appears relative to the trigger.
<script setup lang="ts">
import { Button } from "@dicehub/phi/components/button";
import { Popover, type PopoverSide } from "@dicehub/phi/components/popover";
const sides: PopoverSide[] = ["bottom", "top", "left", "right"];
</script>
<template>
<div class="flex flex-wrap gap-4">
<Popover v-for="side in sides" :key="side">
<Popover.Trigger as-child>
<Button variant="secondary">{{ side }}</Button>
</Popover.Trigger>
<Popover.Content :side="side">
<Popover.Title>{{ side }}</Popover.Title>
<Popover.Description>Popover on {{ side }}.</Popover.Description>
</Popover.Content>
</Popover>
</div>
</template>Custom Content
Popovers can contain custom layouts with avatars, buttons, and other rich content.
<script setup lang="ts">
import { Button } from "@dicehub/phi/components/button";
import { Popover } from "@dicehub/phi/components/popover";
</script>
<template>
<Popover>
<Popover.Trigger as-child>
<Button>User Profile</Button>
</Popover.Trigger>
<Popover.Content class-name="w-64">
<div class="flex items-center gap-3">
<div class="size-10 rounded-full bg-recessed" />
<div>
<Popover.Title>Jane Doe</Popover.Title>
<p class="text-sm text-subtle">[email protected]</p>
</div>
</div>
<div class="mt-3 flex gap-2 border-t pt-3">
<Button variant="secondary" size="sm" class="flex-1">Profile</Button>
<Popover.Close as-child>
<Button variant="ghost" size="sm" class="flex-1">Sign Out</Button>
</Popover.Close>
</div>
</Popover.Content>
</Popover>
</template>Open on Hover
Use openOnHover on the trigger and add a delay when hover-triggered content is useful.
<script setup lang="ts">
import { Button } from "@dicehub/phi/components/button";
import { Popover } from "@dicehub/phi/components/popover";
</script>
<template>
<Popover>
<Popover.Trigger as-child open-on-hover :delay="200">
<Button variant="secondary">Hover Me</Button>
</Popover.Trigger>
<Popover.Content>
<Popover.Title>Hover Triggered</Popover.Title>
<Popover.Description>
This popover opens on hover with a 200ms delay. It can still contain
interactive content like buttons and links.
</Popover.Description>
<div class="mt-3">
<Popover.Close as-child>
<Button variant="secondary" size="sm">Got it</Button>
</Popover.Close>
</div>
</Popover.Content>
</Popover>
</template>Virtual Anchor
Use anchor on Popover.Content to position against a custom element or virtual point.
<script setup lang="ts">
import { computed, nextTick, ref, shallowRef } from "vue";
import { Button } from "@dicehub/phi/components/button";
import { Popover, type PopoverOpenChangeDetails } from "@dicehub/phi/components/popover";
const selectedRow = ref<string | null>(null);
const selectedRowElement = shallowRef<HTMLTableRowElement | null>(null);
const anchor = computed(() => {
const row = selectedRowElement.value;
return row
? {
contextElement: row,
getBoundingClientRect: () => row.getBoundingClientRect(),
}
: undefined;
});
async function editRow(row: HTMLTableRowElement, id: string) {
selectedRowElement.value = row;
await nextTick();
selectedRow.value = id;
}
function onOpenChange(details: PopoverOpenChangeDetails) {
if (!details.open) {
selectedRow.value = null;
selectedRowElement.value = null;
}
}
</script>
<template>
<table>
<!-- call editRow(rowElement, id) from each row action -->
</table>
<Popover :open="Boolean(selectedRow)" @open-change="onOpenChange">
<Popover.Content side="left" :anchor="anchor">
<Popover.Title>Edit {{ selectedRow }}</Popover.Title>
<Popover.Description>
The popover anchors to the selected row, not the icon button.
</Popover.Description>
<Popover.Close as-child>
<Button variant="secondary" size="sm">Close</Button>
</Popover.Close>
</Popover.Content>
</Popover>
</template>API Reference
Popover
The root component that manages popover state.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
v-model:open | boolean | - | Vue controlled open state. |
open | boolean | - | Controlled open state. |
defaultOpen | boolean | false | Initial open state for uncontrolled usage. |
@open-change | (details: PopoverOpenChangeDetails) => void | - | Emitted whenever the popover opens or closes. |
modal | boolean | false | Disables outside interaction and traps focus while open. |
closeOnEscape | boolean | true | Close when Escape is pressed. |
closeOnInteractOutside | boolean | true | Close when interacting outside the popover. |
portalled | boolean | true | Proxy tab order when content is portalled. |
positioning | PopoverRootProps['positioning'] | - | Advanced Ark positioning options for the content surface. |
Popover.Trigger
A button that opens the popover when clicked. Use as-child to render your own element.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render the slotted element as the trigger. |
openOnHover | boolean | false | Open the popover on hover and focus. |
delay | number | 0 | Hover/focus delay in milliseconds. |
value | string | - | Value for identifying the active trigger. |
className | string | - | Additional CSS classes. |
Popover.Content
The container for popover content. Controls its side, alignment, and offset.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
side | "top" | "bottom" | "left" | "right" | "bottom" | Which side of the trigger the popover appears on. |
align | "start" | "center" | "end" | "center" | Alignment along the trigger. |
sideOffset | number | 8 | Distance between the trigger and popover in pixels. |
alignOffset | number | 0 | Additional offset along the alignment axis. |
positionMethod | "absolute" | "fixed" | "absolute" | CSS positioning strategy. |
anchor | HTMLElement | VirtualElement | (() => ...) | - | Custom element or virtual element to anchor against. |
container | string | HTMLElement | "body" | Teleport target for the portalled content. |
className | string | - | Additional CSS classes for the content panel. |
Popover.Title
A heading that labels the popover for accessibility.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes. |
Popover.Description
A paragraph providing additional context about the popover content.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes. |
Popover.Close
A button that closes the popover when clicked. Use as-child to render your own element.
| Prop / Event | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render the slotted element as the close trigger. |
className | string | - | Additional CSS classes. |