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.

Lock Scroll
Flip
Playground.vuevue
<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.

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

Status

Deployment healthy

Last checked 2 minutes ago

Latency

42 ms

Error rate

0.02%

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

Data: true, false, true

Notification preferences

Push notificationsAlerts in the menu bar.
Weekly digestSent Mondays at 8am local time.
Play sound
Settings.vuevue
<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.


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

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

NameEmailRoleActions
Ada Lovelaceada@example.comAdmin

Delete Ada Lovelace?

This removes the row from the local table. The trigger button lives outside the popover.

Grace Hoppergrace@example.comOwner

Delete Grace Hopper?

This removes the row from the local table. The trigger button lives outside the popover.

Katherine Johnsonkatherine@example.comViewer

Delete Katherine Johnson?

This removes the row from the local table. The trigger button lives outside the popover.

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

NameTypeTSDefaultDescription
positionstringstring'bottom-start'Preferred side and alignment around the trigger.
offsetnumbernumber8Gap in pixels between the trigger and the popover.
collisionPaddingnumbernumber8Viewport 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.
flipbooleanbooleantrueAllow the popover to flip to the opposite side when it would collide with the viewport.
lockScrollbooleanbooleanfalseLock 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.
hoverCloseDelaynumbernumber140Delay in milliseconds before a hover-triggered popover closes after pointer or focus leaves.
arrowbooleanbooleantrueShow a small arrow pointing back to the trigger.
widthstringstring'min-w-[14rem] max-w-[20rem]'Tailwind width utility(ies) for the panel.
paddingstringstring'p-4'Tailwind padding utility for the panel.
labelstringstring'More'Trigger button label (use the #trigger slot for richer content).
triggerIdstringstring''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

NameScopeDescription
#triggerReplaces 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.