usePopover
A composable for native popover API behavior with CSS anchor positioning.
Installation
Install the Popover plugin to set an app-wide positioning adapter. Without it, every usePopover() call uses V0PopoverAdapter (CSS anchor positioning). Per-instance adapter still wins over the plugin. Tooltip surfaces can set a different adapter on createTooltipPlugin that only they see.
import { createApp } from 'vue'
import { createPopoverPlugin } from '@vuetify/v0'
import { FloatingUIPopoverAdapter } from '@vuetify/v0/popover/adapters/floating-ui'
import App from './App.vue'
const app = createApp(App)
app.use(createPopoverPlugin({ adapter: new FloatingUIPopoverAdapter() }))
app.mount('#app')Usage
usePopover manages a popover’s open/close state, generates CSS anchor positioning styles, and synchronizes reactive state with native popover toggle events. Spread anchorStyles on the activator, contentAttrs and contentStyles on the content element, call attachAnchor() on the trigger, and attach() on the content to wire the positioning adapter and native popover lifecycle.
<script setup lang="ts">
import { usePopover } from '@vuetify/v0'
import { useTemplateRef } from 'vue'
const trigger = useTemplateRef('trigger')
const content = useTemplateRef('content')
const {
isOpen,
toggle,
attach,
attachAnchor,
anchorStyles,
contentAttrs,
contentStyles,
} = usePopover({ positionArea: 'bottom' })
attachAnchor(trigger)
attach(content)
</script>
<template>
<button ref="trigger" :style="anchorStyles" @click="toggle">
{{ isOpen ? 'Close' : 'Open' }}
</button>
<div
ref="content"
v-bind="contentAttrs"
:style="contentStyles"
>
Popover content
</div>
</template>Adapters
Adapters let you swap the underlying positioning engine without changing your application code.
| Adapter | Import | Description |
|---|---|---|
V0PopoverAdapter | @vuetify/v0 | CSS anchor positioning (default, zero runtime dependency) |
FloatingUIPopoverAdapter | @vuetify/v0/popover/adapters/floating-ui | Floating UI↗︎ JS measurement — flip() covers overflow |
FloatingUIPopoverAdapter is subpath-only so @floating-ui/dom stays out of the main barrel. Install the peer, then pass an instance via the adapter option. positionTry is ignored; flip() covers the overflow intent. Pass middleware to the constructor to override the default [offset(8), flip(), shift({ padding: 8 })].
pnpm add @floating-ui/domnpm install @floating-ui/domyarn add @floating-ui/dombun add @floating-ui/domimport { usePopover } from '@vuetify/v0'
import { FloatingUIPopoverAdapter } from '@vuetify/v0/popover/adapters/floating-ui'
const popover = usePopover({ adapter: new FloatingUIPopoverAdapter() })App-wide, skip the per-instance option and install the plugin once:
import { createPopoverPlugin } from '@vuetify/v0'
import { FloatingUIPopoverAdapter } from '@vuetify/v0/popover/adapters/floating-ui'
app.use(createPopoverPlugin({ adapter: new FloatingUIPopoverAdapter() }))Architecture
usePopover builds on useEventListener for native toggle event synchronization. It is a standalone composable — not part of the compound Popover component — making it ideal for building select, combobox, tooltip, and menu components directly.
Options
| Option | Type | Default | Notes |
|---|---|---|---|
id | string | auto | Base ID for anchor name and popover id. Auto-generated if not provided |
positionArea | MaybeRefOrGetter<string> | 'bottom' | CSS position-area value — controls where the content appears relative to the anchor |
positionTry | MaybeRefOrGetter<string> | 'most-width bottom' | CSS position-try-fallbacks value — fallback positions when the primary area overflows |
isOpen | Ref<boolean> | — | External ref for bidirectional open state (e.g., from defineModel) |
openDelay | MaybeRefOrGetter<number> | 0 | Milliseconds to wait before opening the popover |
closeDelay | MaybeRefOrGetter<number> | 0 | Milliseconds to wait before closing the popover |
adapter | PopoverAdapter | new V0PopoverAdapter() | Positioning engine. Resolution: per-instance, then createPopoverPlugin, then CSS anchor positioning — see Adapters |
Reactivity
| Property/Method | Reactive | Notes |
|---|---|---|
isOpen | ShallowRef, tracks whether the popover is open | |
open() | - | Open the popover |
close() | - | Close the popover |
toggle() | - | Toggle open/close |
cancel() | - | Cancel any pending open or close transition |
attach(el) | - | Wire native show/hide watch + toggle event sync to a content element |
attachAnchor(el) | - | Register the activator/reference element with the positioning adapter |
anchorStyles | Readonly Ref, CSS anchor-name for the activator element | |
contentAttrs | Readonly Ref, id and popover attribute for the content element | |
contentStyles | Readonly Ref, adapter-owned styles for the content element |
Examples
Open the menu and choose an action. Click outside or press Escape to dismiss.
Bring your own positioning engine
usePopover positions content with CSS anchor positioning by default (V0PopoverAdapter) — no JavaScript measurement, no runtime dependency. For Firefox ESR and Safari before version 26, reach for the shipped FloatingUIPopoverAdapter first. The sketch below is the adapter shape if you want to wrap a different engine (Popper, or your own) rather than import the first-party one. Per-call state lives in the setup() closure so a shared instance stays re-entrant — do not assign this.dispose. Native [popover] is position: fixed with inset: 0; margin: auto, so any JS engine that writes top/left must unset those, and if the engine has a strategy option it must be 'fixed'.
import { IN_BROWSER, isNullOrUndefined, PopoverAdapter } from '@vuetify/v0'
import { onScopeDispose, shallowRef, watch } from 'vue'
import type { PopoverAdapterContext } from '@vuetify/v0'
export class MyPopoverAdapter extends PopoverAdapter {
setup (context: PopoverAdapterContext) {
function positionStyles (top: string, left: string): Record<string, string> {
return {
'position': 'fixed',
'margin': 'unset',
'inset': 'unset',
top,
left,
}
}
const styles = shallowRef(positionStyles('0px', '0px'))
function reposition () {
if (!IN_BROWSER) return
const anchor = context.anchorEl.value
const content = context.contentEl.value
if (isNullOrUndefined(anchor) || isNullOrUndefined(content)) return
if (!context.isOpen.value) return
const rect = anchor.getBoundingClientRect()
const size = content.getBoundingClientRect()
const { side, align } = context.placement.value
const gap = 8
let top = rect.bottom + gap
let left = rect.left
if (side === 'top') {
top = rect.top - size.height - gap
} else if (side === 'left') {
top = rect.top
left = rect.left - size.width - gap
} else if (side === 'right') {
top = rect.top
left = rect.right + gap
}
if (align === 'end') {
left = rect.right - size.width
} else if (align === 'center' && (side === 'top' || side === 'bottom')) {
left = rect.left + (rect.width - size.width) / 2
}
styles.value = positionStyles(`${top}px`, `${left}px`)
}
const stopWatch = watch(
[context.anchorEl, context.contentEl, context.isOpen, context.placement],
reposition,
{ immediate: true },
)
onScopeDispose(stopWatch, true)
return styles
}
}import { usePopover } from '@vuetify/v0'
import { MyPopoverAdapter } from './my-popover-adapter'
const popover = usePopover({ adapter: new MyPopoverAdapter() })Everything else is unchanged — attach(), attachAnchor(), contentAttrs, and anchorStyles all work the same way regardless of which adapter is active. contentStyles becomes whatever the adapter’s setup() returns instead of the CSS anchor-positioning declarations.
The Popover component and the Select, Tooltip, and createCombobox built on top of usePopover all accept the same adapter option (positionAdapter on createCombobox, since it already has its own filtering adapter) and forward it through, so swapping the positioning engine for one of those doesn’t require dropping down to usePopover directly.
FAQ
Reach for usePopover when you want full control over a select, combobox, tooltip, or menu surface built from your own markup. Use the Popover component when its slots and transitions are enough.
attach(el) wires the native popover’s toggle event back into isOpen. Without it, the browser’s light dismiss (outside click, Escape) would close the popover but your reactive state would drift out of sync.
No. contentAttrs registers a native auto popover, so the browser handles light dismiss — outside click and Escape — for free. Reach for useClickOutside only when you wire dismissal manually instead of using the native popover.
Pass openDelay and closeDelay (ms) in the options. Call cancel() to abort a pending open or close transition before it fires.
Set positionArea (e.g. 'bottom') for the primary placement and positionTry for the fallback positions the browser flips to when that area overflows — CSS anchor positioning handles it with no JavaScript layout math.
Yes. Import FloatingUIPopoverAdapter from @vuetify/v0/popover/adapters/floating-ui and pass it as the adapter option — see Adapters. It requires the @floating-ui/dom peer; the CSS default (V0PopoverAdapter) stays zero-dependency. For a different engine, see Bring your own positioning engine.