Popover
A floating panel anchored to a trigger, for secondary content like settings, details, or small forms that shouldn't take over the screen. It opens on click, positions itself next to the trigger, and closes when you click outside.
Popover is a compound component: you compose three parts.
Anatomy
Popover ← the root; controls placement and offset
├── PopoverTrigger ← the clickable element that opens it
└── PopoverContent ← the floating panel (controls its own size)The trigger exposes an open slot prop (the current open state); the content exposes a close slot prop (call it to dismiss the panel from inside).
Usage
A common popover is contextual help: an info button that explains a setting without cluttering the page.
<script setup lang="ts">
import { Popover, PopoverTrigger, PopoverContent, Button } from "@dlbcodes/ui";
import { PhInfo } from "@phosphor-icons/vue";
</script>
<template>
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-text-primary"
>Monthly spend limit</span
>
<Popover placement="top">
<PopoverTrigger as-child v-slot="{ open }">
<Button
variant="icon"
size="icon-sm"
aria-label="What is this?"
:class="open ? 'bg-bg-subtle' : ''"
>
<PhInfo class="size-4" />
</Button>
</PopoverTrigger>
<PopoverContent size="sm" class="p-4">
<div class="text-sm text-text-secondary">
The most this workspace can be charged in a billing period.
Requests beyond the limit are paused until the next cycle.
</div>
</PopoverContent>
</Popover>
</div>
</template>
Placement
The root's placement prop controls where the panel opens relative to the trigger. It flips automatically if there isn't room.
<script setup lang="ts">
import { Popover, PopoverTrigger, PopoverContent, Button } from "@dlbcodes/ui";
const placements = [
{ p: "top", label: "Top" },
{ p: "bottom", label: "Bottom" },
] as const;
</script>
<template>
<div class="flex flex-wrap gap-3">
<Popover v-for="{ p, label } in placements" :key="p" :placement="p">
<PopoverTrigger as-child>
<Button variant="outline">{{ label }}</Button>
</PopoverTrigger>
<PopoverContent size="xs" class="p-4">
<div class="text-sm text-text-secondary">
Opens to the {{ label.toLowerCase() }}.
</div>
</PopoverContent>
</Popover>
</div>
</template>
Closing from inside
PopoverContent exposes a close function via its default slot, useful for "Save" / "Cancel" actions that should dismiss the panel. This makes the popover a natural home for small inline edits.
<script setup lang="ts">
import { ref } from "vue";
import {
Popover,
PopoverTrigger,
PopoverContent,
Field,
FieldLabel,
FieldContent,
Input,
Button,
} from "@dlbcodes/ui";
const label = ref("Untitled board");
const draft = ref(label.value);
const saveAnd = (close: () => void): void => {
label.value = draft.value;
close();
};
const cancelAnd = (close: () => void): void => {
draft.value = label.value;
close();
};
</script>
<template>
<div class="flex items-center gap-2">
<span class="text-sm text-text-secondary">Board:</span>
<span class="text-sm font-medium text-text-primary">{{ label }}</span>
<Popover placement="bottom-start">
<PopoverTrigger as-child v-slot="{ open }">
<Button
variant="ghost"
size="sm"
:class="open ? 'bg-bg-subtle' : ''"
>
Rename
</Button>
</PopoverTrigger>
<PopoverContent size="sm" class="p-4">
<template #default="{ close }">
<div class="flex flex-col gap-3">
<Field>
<FieldLabel>Board name</FieldLabel>
<FieldContent>
<Input v-model="draft" variant="contrast" />
</FieldContent>
</Field>
<div class="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
@click="cancelAnd(close)"
>
Cancel
</Button>
<Button size="sm" @click="saveAnd(close)"
>Save</Button
>
</div>
</div>
</template>
</PopoverContent>
</Popover>
</div>
</template>
Sizing
PopoverContent takes a size prop controlling the panel width: fit (to content), full, or a fixed step (3xs through lg). Unlike a select menu, a popover isn't matched to the trigger's width; it's sized for its content.
Appearance
The panel uses the theme's --popover surface with a --border outline and a shadow, and its corners follow --radius. It shares this look with Dropdown and Modal, so floating surfaces match, including in dark mode. See Theming.
Props
Popover
| Prop | Type | Default | Description |
|---|---|---|---|
placement | "bottom-start" | "bottom-end" | "bottom" | "top-start" | "top-end" | "top" | "left" | "right" | "bottom-end" | Where the panel opens relative to the trigger. |
offset | number | 4 | Gap (px) between trigger and panel. |
class | string | none | Classes merged onto the root. |
PopoverTrigger
The element that opens the panel. It has two modes:
- Default: the trigger renders its own button, and the slot is its content. Use for bare content (a label, or an icon plus label).
asChild: the trigger projects the popover behavior onto the single child you provide, instead of rendering its own button. Use when the child is already an interactive element (yourButton, or an icon button), to avoid nesting a<button>inside a<button>:
<PopoverTrigger as-child>
<Button variant="outline">Open</Button>
</PopoverTrigger>Exposes open (boolean) via its default slot.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Project popover behavior onto the child instead of rendering a button. |
class | string | none | Classes merged onto the trigger (default mode only). |
PopoverContent
| Prop | Type | Default | Description |
|---|---|---|---|
size | "fit" | "full" | "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xs" | Panel width. Exposes close via its slot. |
class | string | none | Classes merged onto the panel. |
Accessibility
- Built on Headless UI's
Popover, so focus management,Escapeto close, click-outside dismissal, and ARIA wiring are handled for you. - The trigger is a real button; the panel is keyboard-navigable and returns focus to the trigger on close.
- The default-mode trigger shows a visible keyboard focus ring in the theme's
--ringcolor.