Phi
Popover
@dicehub/phiv1.0.0-beta.1

Popover

An accessible popup anchored to a trigger element, used for displaying rich content like menus, forms, or additional information.

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

TooltipPopover
PurposeShort, non-interactive text labels for identificationRich, interactive content containers
ContentPlain text onlyAny content: links, buttons, forms, images
TriggerHover or focusClick (default) or hover
ARIA Rolerole="tooltip"aria-haspopup
KeyboardNot focusableFocus 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 / EventTypeDefaultDescription
v-model:openboolean-Vue controlled open state.
openboolean-Controlled open state.
defaultOpenbooleanfalseInitial open state for uncontrolled usage.
@open-change(details: PopoverOpenChangeDetails) => void-Emitted whenever the popover opens or closes.
modalbooleanfalseDisables outside interaction and traps focus while open.
closeOnEscapebooleantrueClose when Escape is pressed.
closeOnInteractOutsidebooleantrueClose when interacting outside the popover.
portalledbooleantrueProxy tab order when content is portalled.
positioningPopoverRootProps['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 / EventTypeDefaultDescription
asChildbooleanfalseRender the slotted element as the trigger.
openOnHoverbooleanfalseOpen the popover on hover and focus.
delaynumber0Hover/focus delay in milliseconds.
valuestring-Value for identifying the active trigger.
classNamestring-Additional CSS classes.

Popover.Content

The container for popover content. Controls its side, alignment, and offset.

Prop / EventTypeDefaultDescription
side"top" | "bottom" | "left" | "right""bottom"Which side of the trigger the popover appears on.
align"start" | "center" | "end""center"Alignment along the trigger.
sideOffsetnumber8Distance between the trigger and popover in pixels.
alignOffsetnumber0Additional offset along the alignment axis.
positionMethod"absolute" | "fixed""absolute"CSS positioning strategy.
anchorHTMLElement | VirtualElement | (() => ...)-Custom element or virtual element to anchor against.
containerstring | HTMLElement"body"Teleport target for the portalled content.
classNamestring-Additional CSS classes for the content panel.

Popover.Title

A heading that labels the popover for accessibility.

Prop / EventTypeDefaultDescription
classNamestring-Additional CSS classes.

Popover.Description

A paragraph providing additional context about the popover content.

Prop / EventTypeDefaultDescription
classNamestring-Additional CSS classes.

Popover.Close

A button that closes the popover when clicked. Use as-child to render your own element.

Prop / EventTypeDefaultDescription
asChildbooleanfalseRender the slotted element as the close trigger.
classNamestring-Additional CSS classes.