@rozie-ui/popover-vue
| Install | |
|---|---|
npm install @rozie-ui/popover-vue |
|
| Latest Version: | 0.3.3 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 15, 2026 |
| Links: | GitHub · npm |
@rozie-ui/popover-vue
Idiomatic vue Popover — a headless floating primitive for tooltips and popovers, wrapping @floating-ui/dom for collision-aware positioning (offset / flip / shift / arrow) with live autoUpdate tracking. You bring the anchor (the anchor slot) and the floating content (the default slot); Popover owns placement, the open/close gesture (trigger: click / hover / focus), dismissal (Escape + click-outside), the WAI-ARIA wiring (tooltip vs dialog), and a two-way open model — compiled from one Rozie source. Every visual value is a CSS custom property, so it re-skins to any design system. This package is generated; do not edit src/ by hand.
Install
npm i @rozie-ui/popover-vue @floating-ui/dom
Peer dependencies: vue + @floating-ui/dom. Install them alongside this package.
Required peers — beyond the framework peer above, this package requires these non-optional peers to actually render:
@floating-ui/dom^1.7.2— required by@rozie-ui/popover-vue
Install the whole chain in one line:
npm i @rozie-ui/popover-vue @floating-ui/dom
Also installed: @rozie/runtime-vue — Rozie's small, tree-shaken runtime helper package (controllable state, keyboard navigation, event modifiers, and safe interpolation). It arrives as a regular dependency, so npm pulls it for you. Your bundler keeps only the helpers this component actually uses — typically a few hundred bytes to a few KB, minified and gzipped. What's in it and what it costs.
Usage
<script setup lang="ts">
import { ref } from 'vue';
import Popover from '@rozie-ui/popover-vue';
const open = ref(false);
</script>
<template>
<Popover v-model:open="open" trigger="click" placement="bottom" :offset="8" arrow>
<template #anchor="{ open, toggle, panelId }">
<button @click="toggle" :aria-expanded="open" :aria-controls="panelId">Menu</button>
</template>
<div>Floating content</div>
</Popover>
</template>
Positioning against an external element
Pass reference to position the panel against an element you own (or a Floating UI virtual element, e.g. a pointer position) instead of the built-in anchor. Use trigger="manual", drive open yourself, and put the trigger ARIA on your own element. aria-controls points at idBase + '-panel'. A click on the referenced element is not an outside click, so your toggle closes the panel. The outside-click dismissal is decided after your own handlers run, so a handler that repoints reference at another element it was clicked on (keeping open true) moves the panel there. If the referenced element leaves the document while open, the panel closes.
<script setup lang="ts">
import { ref } from 'vue';
import Popover from '@rozie-ui/popover-vue';
const open = ref(false);
const target = ref<HTMLElement | null>(null);
const onClick = (e: MouseEvent) => {
target.value = e.currentTarget as HTMLElement;
open.value = !open.value;
};
</script>
<template>
<button :aria-expanded="open" aria-controls="details-popover-panel" @click="onClick">Details</button>
<Popover v-model:open="open" trigger="manual" :reference="target" id-base="details-popover">
<div>About this item</div>
</Popover>
</template>
Layout
The .rozie-popover root is display: contents, so Popover adds no box of its own: the anchor wrapper sits in the flow and the panel floats. A class or style you pass reaches the root, which is enough for inherited properties (color, font, the --rozie-popover-* tokens), but box properties (margin, width, flex, position, grid-area) have nothing to act on there. To place a popover in a flex row or grid, wrap it in your own element and style the wrapper.
Theming
Every visual value is a --rozie-popover-* CSS custom property (background, border, radius, shadow, padding, z-index, max-width, arrow size) — override any of them at any ancestor scope to match your design system.
Props
| Name | Type | Default | Two-way (model) | Required | Description |
|---|---|---|---|---|---|
open |
Boolean |
false |
✓ | Whether the floating content is open. The sole model: true prop, and its change event is the only change signal Popover fires. Bind it two-way — Vue v-model:open, React/Solid open + onOpenChange, Svelte bind:open, Angular [(open)], Lit the open property + the open-change event — and Popover writes the new state back whenever the trigger, a dismissal or the handle toggles it. Left unbound it falls back to an uncontrolled default. |
|
placement |
String |
"bottom" |
Floating UI placement of the content relative to the anchor — one of top/right/bottom/left, each optionally suffixed -start/-end (e.g. bottom-start). With disableFlip off, the content flips to the opposite side when it would overflow the viewport, and a left/right placement with no room on either side falls back to below or above the anchor. Reconciled at runtime. |
||
trigger |
String |
"click" |
How the anchor opens the content: 'click' toggles on click, 'hover' opens on pointer-enter and closes on pointer-leave (tooltip-style), 'focus' opens on focus and closes on blur, or 'manual' for a composing component that drives open itself — every built-in gesture handler no-ops. Drives both the gesture handlers and the ARIA: 'click' sets aria-haspopup/aria-expanded/aria-controls on the anchor wrapper; 'hover'/'focus' are tooltips (role="tooltip" panel, aria-describedby on the wrapper, no popup claim); 'manual' makes no anchor ARIA claim. The wrapper is not focusable, so put the matching attributes on your own focusable trigger too — the anchor slot passes open, panelId and popupRole for exactly that. The aria-haspopup value is the popupRole prop (default 'dialog'). |
||
offset |
Number |
8 |
Distance in pixels between the anchor and the floating content (the Floating UI offset middleware). Reconciled at runtime. |
||
disableFlip |
Boolean |
false |
Disable the Floating UI flip middleware. By default the content flips to the opposite side of the anchor when it would overflow the viewport, and a left/right placement with no room on either side falls back to below or above the anchor; set this to keep it pinned to placement regardless. |
||
disableShift |
Boolean |
false |
Disable the Floating UI shift middleware. By default the content shifts to stay within the viewport (a left/right placement that fits nowhere slides back across the anchor), and its width is capped at the available width Popover measures and publishes on the panel as --rozie-popover-available-width; set this to keep it strictly aligned to the anchor and drop that measured cap. |
||
arrow |
Boolean |
false |
Opt in to a positioned arrow element. When set, Popover renders an arrow <div> and runs the Floating UI arrow middleware against it so it points at the anchor. Style it via the --rozie-popover-* arrow CSS custom properties. |
||
disabled |
Boolean |
false |
Disable the control entirely: the trigger no longer opens the content and any open content is suppressed. | ||
modal |
Boolean |
false |
Opt in to modal dialog semantics for a click popover. Off by default: a click popover is a non-modal, click-outside-dismissable layer, so its panel is rendered role-neutral (the slot content owns its own ARIA role — e.g. a role="menu") and carries NO aria-modal. Set modal for a genuinely modal dialog popover: the panel then gets role="dialog" + aria-modal="true". Note: Popover ships no focus trap (it stays a minimal headless primitive); if you set modal, provide your own focus containment so the aria-modal claim holds. Ignored for hover/focus triggers (always tooltip-flavored). |
||
strategy |
String |
"absolute" |
Floating UI positioning strategy — 'absolute' (default) or 'fixed'. Use 'fixed' to escape a scrollable/overflow-clipping ancestor (e.g. a sticky table header). Reconciled at runtime. | ||
bare |
Boolean |
false |
Suppress the floating panel's own chrome (background, border, border-radius, box-shadow, padding) so a composing component can supply its own instead. Off by default — the panel keeps its standard --rozie-popover-* chrome tokens. |
||
disablePositioning |
Boolean |
false |
Render the floating panel in normal document flow instead of computing a floating position — no computePosition call and no autoUpdate tracking is ever started. For a composing component that already controls the panel's layout (e.g. an inline consumer) rather than a genuinely floating popover. |
||
keepMounted |
Boolean |
false |
Render the floating panel hidden instead of unmounting it while closed, so a composing component whose panel content owns scroll state (e.g. a virtualizer) keeps its DOM across a close/open cycle. A one-shot position computation runs once at mount so the hidden panel already carries correct coordinates before the first open. | ||
matchWidth |
Boolean |
false |
Match the floating panel's width exactly to the anchor's width, via the Floating UI size middleware. Writes the panel's width style only — never touches height. |
||
disableDismiss |
Boolean |
false |
Suppress Popover's own Escape-key and click-outside dismissal listeners while true. For a composing component that drives open itself and needs to temporarily veto Popover's independent dismissal — e.g. while a host sub-surface anchored to (but not nested inside) the composed control legitimately holds focus. Off by default; existing trigger="manual" consumers relying on real click-outside dismissal are unaffected unless they opt in. |
||
popupRole |
String |
"dialog" |
The kind of popup the panel content is, announced as aria-haspopup on the anchor wrapper of a click popover: 'dialog' (the default), 'menu', 'listbox', 'tree' or 'grid'; any other value is treated as 'dialog'. Set 'menu' when the panel hosts a menu (your content carries role="menu"), so a menu button keeps the click trigger and its focus return. The anchor slot passes the same value as popupRole (with open and panelId) so you can put aria-haspopup / aria-expanded / aria-controls on your own focusable trigger. It is null for the tooltip triggers ('hover' / 'focus'), which claim no popup; for 'manual' it is the prop value, for the trigger you own. |
||
idBase |
String |
'' |
Id base for the floating panel, whose id is idBase + '-panel' — also exposed to the anchor slot as panelId, so your trigger can set aria-controls (click) or aria-describedby (tooltip) to it. Leave it empty (the default) and each instance generates a unique id base after mount (rozie-popover-<n>), so several popovers on one page never share a panel id; set it when you need a stable, predictable id. On Lit the panel lives in the element's shadow root, so an id reference from light DOM, including your slotted anchor content, cannot resolve to it; there the anchor wrapper's own attributes, which sit inside the shadow root, carry the reference. Named idBase (not id) to avoid shadowing HTMLElement.id on the Lit custom element. |
||
reference |
Element | Object |
null |
Position the content against an external reference instead of the built-in anchor wrapper: either a DOM Element another component owns (e.g. a calendar event element) or a Floating UI virtual element — an object with a getBoundingClientRect() method and an optional contextElement — e.g. to open at a pointer position. The reference is measured and tracked with Floating UI's autoUpdate and reconciled at runtime; null (the default) keeps the built-in anchor. A click on a referenced Element does not count as an outside click (so a consumer toggle on it closes the panel); with a virtual element only the anchor wrapper and the panel count as inside. You own the trigger ARIA on your own element (aria-haspopup / aria-expanded, plus aria-controls pointing at idBase + '-panel'), typically with trigger='manual' and a two-way-bound open. If a referenced Element is removed from the document while open, the popover closes. Pass a stable value — a new object on every render restarts tracking. |
Events
| Event | Payload | Description |
|---|---|---|
update:open |
boolean |
The open model's change event, and the only change signal: fired whenever the open state changes — a click/hover/focus trigger gesture, an Escape or click-outside dismissal, or a programmatic show/hide/toggle. Bind it with v-model:open. |
Imperative handle
Beyond props, the component exposes imperative methods (declared once in the Rozie source via $expose). Grab a handle with the native ref mechanism and call them directly:
| Method | Description |
|---|---|
show |
Open the floating content (no-op when disabled). Emits change and updates the open model. |
hide |
Close the floating content. Emits change and updates the open model. |
toggle |
Flip the open state (no-op when disabled). Emits change and updates the open model. |
reposition |
Recompute the floating position immediately (the Floating UI computePosition pass). Useful after content size changes that autoUpdate does not observe. |
<script setup>
import { ref } from 'vue';
const pop = ref(); // template ref
</script>
<template>
<Popover ref="pop" v-model:open="open"> ... </Popover>
<button @click="pop.show()">Open</button>
<button @click="pop.reposition()">Reposition</button>
</template>
Slots
| Slot | Params | Bind as |
|---|---|---|
| anchor | open, toggle, show, hide, panelId, popupRole | <template #anchor="params"> |
| (default) | default slot |
Related Packages
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.3.3 | ^3.4 | MIT | |
| 0.3.2 | ^3.4 | MIT | |
| 0.3.1 | ^3.4 | MIT | |
| 0.3.0 | ^3.4 | MIT | |
| 0.2.1 | ^3.4 | MIT | |
| 0.2.0 | ^3.4 | MIT | |
| 0.1.5 | ^3.4 | MIT | |
| 0.1.4 | ^3.4 | MIT | |
| 0.1.3 | ^3.4 | MIT | |
| 0.1.2 | ^3.4 | MIT | |
| 0.1.1 | ^3.4 | MIT | |
| 0.1.0 | ^3.4 | MIT |