@rozie-ui/popover-vue

Idiomatic Vue popover/tooltip wrapping @floating-ui/dom — one Rozie source compiled to Vue.
2,266 1
Install
npm install @rozie-ui/popover-vue
Latest Version:0.3.3
Vue:3
License:MIT
Last Updated:Oct 15, 2026
Links: GitHub  ·  npm
Maintainer: serpentblade

@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

v-float

Vue 3 port of Floating UI - a library for positioning floating elements

2,353 7
@floating-ui/vue

Floating UI for Vue

106,918,631 32,773
vue-afloat

Tooltips, popovers, dropdown, menus... for Vue

1,676
@headlessui-float/vue

Easily use Headless UI for Vue 3 with Floating UI (Popper.js)

1,720,313 348
@headlessui-float/nuxt

Easily use Headless UI for Nuxt 3 with Floating UI (Popper.js)

6,399 348

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