Component
Popover
<DomPopover>A floating panel anchored to a trigger. Built on the HTML Popover API so it sits in the top layer — no parent overflow, transform, or z-index can clip it.
Playground
Try every prop live
Popover playground
Edit props in the inspector — the live panel and source/data tabs stay in sync.
Popover body content.
<script setup>
import { reactive } from 'vue';
import { DomPopover } from '@getdom/studio/vue';
const data = reactive({
"position": "bottom-start",
"offset": 8,
"collisionPadding": 8,
"floatingMode": "viewport",
"flip": true,
"lockScroll": false,
"trigger": "click",
"hoverCloseDelay": 140,
"arrow": true,
"width": "min-w-[14rem] max-w-[20rem]",
"padding": "p-4",
"label": "Quick facts",
"triggerId": ""
});
</script>
<template>
<DomPopover
v-bind="data"
/>
</template>Demo
Quick info
The simplest case — a button that opens a panel with text content.
Pay once. Use forever.
DOM Studio is licensed for life. Every component you see ships with the headless layer + a Vue wrapper.
<script setup>
import { DomPopover } from '@getdom/studio/vue';
</script>
<template>
<DomPopover label="Quick facts">
<h3 class="text-sm font-semibold tracking-tight text-fg">Pay once. Use forever.</h3>
<p class="mt-2 text-muted-fg">
DOM Studio is licensed for life. Every component you see ships with the headless layer + a Vue wrapper.
</p>
</DomPopover>
</template>
Demo
Open on hover
Set trigger="hover" for richer tooltip-like panels that open on pointer enter or focus.
Deployment healthy
Last checked 2 minutes ago
Latency
42 ms
Error rate
0.02%
<script setup>
import { DomPopover } from '@getdom/studio/vue';
</script>
<template>
<DomPopover
trigger="hover"
position="top"
width="w-72"
padding="p-0"
>
<template #trigger>
<span class="inline-flex h-10 items-center gap-2 rounded-full bg-secondary px-4 text-sm font-medium text-fg ring-1 ring-border transition hover:bg-accent">
<span class="size-2 rounded-full bg-success"></span>
Status
</span>
</template>
<div class="p-4">
<div class="flex items-center gap-3">
<span class="grid size-9 shrink-0 place-items-center rounded-full bg-success/15 text-success">
<svg viewBox="0 0 24 24" class="size-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5" />
</svg>
</span>
<div class="min-w-0">
<p class="text-sm font-semibold text-fg">Deployment healthy</p>
<p class="mt-0.5 text-xs text-muted-fg">Last checked 2 minutes ago</p>
</div>
</div>
<div class="mt-4 grid grid-cols-2 gap-3 text-xs">
<div>
<p class="text-muted-fg">Latency</p>
<p class="mt-1 font-medium text-fg">42 ms</p>
</div>
<div>
<p class="text-muted-fg">Error rate</p>
<p class="mt-1 font-medium text-fg">0.02%</p>
</div>
</div>
</div>
</DomPopover>
</template>
Demo
Settings panel
Popover bodies can host any controls — including the form inputs from the library.
Notification preferences
<script setup>
import { ref } from 'vue';
import { DomPopover, DomToggle } from '@getdom/studio/vue';
const notifications = ref(true);
const digest = ref(false);
const sound = ref(true);
</script>
<template>
<div class="flex flex-col gap-4">
<div>Data: {{ notifications }}, {{ digest }}, {{ sound }}</div>
<DomPopover class="text-left" label="Notifications" position="bottom" width="min-w-[18rem]">
<h3 class="text-sm font-semibold tracking-tight text-fg">Notification preferences</h3>
<div class="mt-3 space-y-3 flex flex-col gap-2">
<DomToggle v-model="notifications" label="Push notifications" description="Alerts in the menu bar." />
<DomToggle v-model="digest" label="Weekly digest" description="Sent Mondays at 8am local time." />
<DomToggle v-model="sound" label="Play sound" />
</div>
</DomPopover>
</div>
</template>
Demo
Actions menu
Use DomMenu inside a popover when the content is an action list. The menu adds roving focus, arrow keys, and selection events.
<script setup>
import { ref } from 'vue';
import { DomMenu, DomPopover } from '@getdom/studio/vue';
const popover = ref(null);
const menu = ref(null);
const lastAction = ref('');
const actions = [
{ label: 'Rename', value: 'rename' },
{ label: 'Duplicate', value: 'duplicate' },
{ label: 'Move to...', value: 'move' },
{ separator: true },
{ label: 'Delete', value: 'delete', tone: 'danger' },
];
function focusMenu() {
requestAnimationFrame(() => {
menu.value?.querySelector('[role="menuitem"]')?.focus();
});
}
function selectAction({ item }) {
lastAction.value = item?.label || '';
popover.value?.close();
}
</script>
<template>
<div class="space-y-3">
<DomPopover ref="popover" label="Actions" position="bottom-end" width="min-w-[12rem]" padding="p-1" @open="focusMenu">
<div ref="menu">
<DomMenu :items="actions" :skin="false" @select="selectAction" />
</div>
</DomPopover>
<p v-if="lastAction" class="text-sm text-muted-fg">Selected {{ lastAction }}.</p>
</div>
</template>
Demo
Scrollable menu with scroll lock
Lock browser scrolling while a long menu is open, and put the scrolling on the popover content rather than the page.
Selected: none
<script setup>
import { ref } from 'vue';
import { DomMenu, DomPopover } from '@getdom/studio/vue';
const popover = ref(null);
const menu = ref(null);
const selected = ref('none');
const items = Array.from({ length: 30 }, (_, index) => ({
label: `Project ${index + 1}`,
value: `project-${index + 1}`,
}));
function focusMenu() {
requestAnimationFrame(() => {
menu.value?.querySelector('[role="menuitem"]')?.focus();
});
}
function onSelect(event) {
selected.value = event.value;
popover.value?.close();
}
</script>
<template>
<div class="space-y-3">
<DomPopover
ref="popover"
label="Switch project"
position="bottom-start"
width="min-w-64"
padding="p-0"
lock-scroll
@open="focusMenu"
>
<div ref="menu" class="max-h-72 overflow-y-auto p-1">
<DomMenu :items="items" :skin="false" @select="onSelect" />
</div>
</DomPopover>
<p class="text-xs text-muted-fg">Selected: <code class="text-fg">{{ selected }}</code></p>
</div>
</template>
Demo
External trigger confirmation
Attach a popover to a button outside the component with triggerId. The default slot receives open, close, and toggle helpers, so row actions can close themselves without storing refs.
| Name | Role | Actions | |
|---|---|---|---|
| Ada Lovelace | ada@example.com | Admin | Delete Ada Lovelace? This removes the row from the local table. The trigger button lives outside the popover. |
| Grace Hopper | grace@example.com | Owner | Delete Grace Hopper? This removes the row from the local table. The trigger button lives outside the popover. |
| Katherine Johnson | katherine@example.com | Viewer | Delete Katherine Johnson? This removes the row from the local table. The trigger button lives outside the popover. |
<script setup>
import { ref } from 'vue';
import { DomButton, DomPopover } from '@getdom/studio/vue';
const rows = ref([
{ id: 1, name: 'Ada Lovelace', email: 'ada@example.com', role: 'Admin' },
{ id: 2, name: 'Grace Hopper', email: 'grace@example.com', role: 'Owner' },
{ id: 3, name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Viewer' },
]);
function triggerId(row) {
return `delete-row-${row.id}`;
}
function remove(row) {
rows.value = rows.value.filter((item) => item.id !== row.id);
}
</script>
<template>
<div class="w-full overflow-hidden rounded-2xl border border-border bg-background">
<table class="w-full text-left text-sm">
<thead class="bg-secondary/70 text-xs uppercase tracking-wider text-muted-fg">
<tr>
<th class="px-4 py-3 font-medium">Name</th>
<th class="px-4 py-3 font-medium">Email</th>
<th class="px-4 py-3 font-medium">Role</th>
<th class="px-4 py-3 text-right font-medium">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-border">
<tr v-for="row in rows" :key="row.id">
<td class="px-4 py-3 font-medium text-fg">{{ row.name }}</td>
<td class="px-4 py-3 text-muted-fg">{{ row.email }}</td>
<td class="px-4 py-3 text-muted-fg">{{ row.role }}</td>
<td class="px-4 py-3 text-right">
<button
:id="triggerId(row)"
type="button"
class="rounded-full px-3 py-1.5 text-sm font-medium text-destructive transition hover:bg-destructive/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
>
Delete
</button>
<DomPopover
:trigger-id="triggerId(row)"
position="start-bottom"
width="w-72"
v-slot="{ close }"
>
<div class="space-y-3">
<div class="space-y-1">
<p class="font-medium text-fg">Delete {{ row.name }}?</p>
<p class="text-sm leading-5 text-muted-fg">
This removes the row from the local table. The trigger button lives outside the popover.
</p>
</div>
<div class="flex justify-end gap-2">
<DomButton type="button" size="sm" variant="secondary" @click="close">Cancel</DomButton>
<DomButton type="button" size="sm" variant="danger" @click="remove(row)">Delete</DomButton>
</div>
</div>
</DomPopover>
</td>
</tr>
</tbody>
</table>
<p v-if="!rows.length" class="px-4 py-8 text-center text-sm text-muted-fg">All rows removed.</p>
</div>
</template>
API
Closing from content
The default slot is scoped with open, close, and toggle. This is useful for confirmation popovers, menus, and external triggers because the content can control its own popover.
<DomPopover :trigger-id="triggerId(row)" v-slot="{ close }">
<DomButton @click="close">Cancel</DomButton>
</DomPopover>Reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
position | string | string | 'bottom-start' | Preferred side and alignment around the trigger. |
offset | number | number | 8 | Gap in pixels between the trigger and the popover. |
collisionPadding | number | number | 8 | Viewport padding used when the panel flips or shifts. |
floatingMode | 'viewport' | 'anchor' | string | 'viewport' | viewport keeps the panel inside the browser; anchor keeps it attached while scrolling. |
flip | boolean | boolean | true | Allow the popover to flip to the opposite side when it would collide with the viewport. |
lockScroll | boolean | boolean | false | Lock browser scrolling while the popover is open. |
trigger | 'click' | 'hover' | 'hover-click' | string | 'click' | How the trigger opens the popover. hover-click opens on hover and pins open on click. |
hoverCloseDelay | number | number | 140 | Delay in milliseconds before a hover-triggered popover closes after pointer or focus leaves. |
arrow | boolean | boolean | true | Show a small arrow pointing back to the trigger. |
width | string | string | 'min-w-[14rem] max-w-[20rem]' | Tailwind width utility(ies) for the panel. |
padding | string | string | 'p-4' | Tailwind padding utility for the panel. |
label | string | string | 'More' | Trigger button label (use the #trigger slot for richer content). |
triggerId | string | string | '' | ID of an external trigger button. When set, DomPopover will not render its own trigger. |
Auto-generated from Popover.props and inline _edit hints.
Slots
| Name | Scope | Description |
|---|---|---|
| #trigger | — | Replaces the trigger button. Omit when using triggerId. |
| #(default) | { open, close, toggle } | Popover body. Receives helpers so content can close itself without storing a component ref. |
Keyboard
- Click triggerToggles the popover.
- Focus triggerOpens the popover when trigger is hover.
- Click outsideLight-dismiss via the native popover API.
- EscCloses the popover.