@floating-ui-plus/vue
@floating-ui-plus/vue
Vue composables and declarative components for positioned, interactive surfaces. It adds interactions, focus management, portals, collections, search, and native top layers.
The package includes @floating-ui-plus/web; installing this package is sufficient.
Attribute policy
Use id for a unique application-owned element and classes for repeated
presentation or app selectors. Preserve the data-* attributes returned by
Floating UI Plus bindings because they describe runtime state (for example,
data-loading) or declarative package behavior. Avoid application-only
data-* attributes that are merely selector hooks.
Install
npm install @floating-ui-plus/vue
# pnpm add @floating-ui-plus/vue
# bun add @floating-ui-plus/vue
Vue 3.3 or later is required.
Tooltip quick start
Use useFloating() when your component owns its elements and markup.
<script setup lang="ts">
import {ref} from 'vue';
import {autoUpdate, dismiss, focus, hover, offset, role, useFloating, vFloating} from '@floating-ui-plus/vue';
const open = ref(false);
const reference = ref<HTMLElement | null>(null), floating = ref<HTMLElement | null>(null);
const tooltip = useFloating(reference, floating, {
open,
onOpenChange: (nextOpen) => (open.value = nextOpen),
middleware: [offset(6)],
whileElementsMounted: autoUpdate,
}).pipe(hover(), focus(), dismiss(), role({role: 'tooltip'}));
</script>
<template>
<button ref="reference" v-bind="tooltip.referenceAttrs">Help</button>
<div v-if="open" ref="floating" v-floating="tooltip" v-bind="tooltip.floatingAttrs">Helpful text</div>
</template>
v-floating applies the positioning output; bind floatingStyles for the upstream-compatible manual path.
Declarative surfaces
Use the component layer for ordinary popovers, menus, and dialogs. The root owns one controller and its descendants receive the corresponding bindings.
<script setup lang="ts">
import {ref} from 'vue';
import {
FloatingClose,
FloatingContent,
FloatingReference,
FloatingRoot,
autoUpdate,
click,
dismiss,
flip,
offset,
shift,
transformOrigin,
} from '@floating-ui-plus/vue';
const open = ref(false);
const options = {
placement: 'bottom-start',
middleware: [
offset(8),
flip({padding: 12}),
shift({padding: 12}),
transformOrigin({padding: 8}),
],
whileElementsMounted: autoUpdate,
};
const plugins = [click(), dismiss()];
</script>
<template>
<FloatingRoot
v-model:open="open"
:options="options"
:plugins="plugins"
>
<FloatingReference>Open settings</FloatingReference>
<FloatingContent class="popover">
Settings
<FloatingClose>Close</FloatingClose>
</FloatingContent>
</FloatingRoot>
</template>
FloatingContent uses the browser Popover API by default. Use
top-layer="none" for an intentionally positioned/custom surface and
as="dialog" for a native dialog. role() controls ARIA semantics only. Add
FloatingPortal only when the surface needs to escape its DOM ancestor;
native top-layer content stays in the same Vue tree and should not also be
teleported.
as="dialog" maps directly to the native <dialog> element. Floating UI Plus
coordinates the reference, positioning, and interactions without replacing
those platform behaviors.
Native dialogs also acquire a ref-counted document scroll lock while open.
The default uses CSS overflow: hidden; touch-event interception is not
installed. Add a touch guard in the host application only for legacy iOS or a
WebView that still requires it.
Native dialogs also receive data-fup-safe-area and the CSS variables
--fup-safe-area-inset-top, --fup-safe-area-inset-right,
--fup-safe-area-inset-bottom, and --fup-safe-area-inset-left. Consume these
variables in surface CSS where a notch or home indicator needs extra space; the
package does not force a padding policy on custom dialog layouts.
Native entry and exit animation
Keep <FloatingContent> in the Vue tree and style the native surface states
directly. A non-zero display or overlay transition with allow-discrete
opts into CSS exit motion; without one, closed content receives hidden
immediately.
.floating-panel {
opacity: 0;
translate: 0 -0.25rem;
transform-origin: var(--floating-transform-origin, 50% 0%);
transition:
opacity 120ms cubic-bezier(0.23, 1, 0.32, 1),
translate 120ms cubic-bezier(0.23, 1, 0.32, 1),
display 120ms allow-discrete,
overlay 120ms allow-discrete;
}
.floating-panel:popover-open {
opacity: 1;
translate: 0 0;
}
@starting-style {
.floating-panel:popover-open {
opacity: 0;
translate: 0 -0.25rem;
}
}
transformOrigin() writes the CSS variable from the final placement and
reference geometry. Keep it after placement-changing middleware; the CSS
fallback remains valid when it is omitted.
For a fixed placement, you may omit the middleware and define
transform-origin in CSS. CSS alone cannot observe flip() or shift()
results, so use transformOrigin() whenever the surface can move to another
side or alignment.
Do not wrap an animated native surface in v-if; removing it bypasses the CSS
exit. Reopening cancels a pending hide. For top-layer="none", use
FloatingTransition or useFloatingTransition and keep the surface mounted
until its close state completes. See the
entry and exit animation guide.
Transient presence stacks
useFloatingPresenceStack() is the Vue adapter for the same framework-neutral
context used by floating-presence-stack. Its methods mirror the Web Component
(add, close, remove, pause, and resume) while snapshot, records,
and paused are reactive refs for template rendering. Pass pauseTarget and
pauseOn to bind opt-in pointer/focus pause and --floating-presence-count
on an application-owned host. The composable does not style that node; size
--floating-presence-hit-span on the target to the current stack and keep a
fixed paused row in application CSS so later cards stay readable.
<script setup lang="ts">
import {ref} from 'vue';
import {useFloatingPresenceStack} from '@floating-ui-plus/vue';
const viewport = ref<HTMLElement>();
const notices = useFloatingPresenceStack<{title: string}>({
limit: 3,
timeout: 5000,
pauseTarget: viewport,
pauseOn: 'pointer focus',
});
notices.add({title: 'Saved'});
</script>
Vue owns the rendered elements, ARIA, transition, and native Top Layer policy.
useFloatingTopLayer() defaults to Popover. Call remove(id) after the
rendered exit transition completes.
For fixed transient surfaces that should still use the browser's native Top
Layer, pair the composable with useFloatingTopLayer() on the rendered
element. It does not apply positioning, so the component remains free to own
its layout CSS.
Search and useQuery()
useSearch() owns request state: debouncing, IME composition, cancellation,
stale-response protection, caching, and cursor pagination. useQuery() binds
that state to an editable input, active result, Enter activation, and floating
list navigation.
Its default semantics is "combobox": the input controls a listbox and
uses virtual focus with aria-activedescendant. Use semantics: "dialog" for
a command palette, or "none" when the rendered UI owns all ARIA.
<script setup lang="ts">
import {
FloatingContent,
FloatingList,
FloatingListItem,
FloatingReference,
FloatingResults,
FloatingRoot,
autoUpdate,
createFuzzySearchSource,
dismiss,
useQuery,
useSearch,
} from '@floating-ui-plus/vue';
type City = {id: string; name: string};
const cities: City[] = [
{id: 'seoul', name: 'Seoul'},
{id: 'tokyo', name: 'Tokyo'},
{id: 'paris', name: 'Paris'},
];
const search = useSearch<City>({
source: createFuzzySearchSource(cities, {keys: [{name: 'name'}]}),
getItemKey: (city) => city.id,
debounceMs: 0,
});
const {
open,
activeIndex,
inputProps,
rolePlugin,
getOptionProps,
getNavigationOptions,
} = useQuery<City>({
search,
getItemLabel: (city) => city.name,
onActivate(city) {
search.controller.setQuery(city.name);
},
});
const options = {placement: 'bottom-start', whileElementsMounted: autoUpdate} as const;
const plugins = [dismiss(), rolePlugin];
const navigationOptions = getNavigationOptions({allowEscape: true});
</script>
<template>
<FloatingRoot v-model:open="open" :options="options" :plugins="plugins">
<FloatingList
v-model:active-index="activeIndex"
navigation
:navigation-options="navigationOptions"
>
<FloatingReference
as="input"
aria-label="City"
autocomplete="off"
v-bind="inputProps"
/>
<FloatingContent>
<FloatingResults :search="search">
<template #results>
<FloatingListItem
v-for="(city, index) in search.items.value"
:key="city.id"
:label="city.name"
:value="city"
v-bind="getOptionProps(city, index)"
>
{{ city.name }}
</FloatingListItem>
</template>
<template #empty>No cities found.</template>
</FloatingResults>
</FloatingContent>
</FloatingList>
</FloatingRoot>
</template>
The fuzzy source, Query composable, and rendered components all come from
@floating-ui-plus/vue. Add loading and error slots on FloatingResults
when the application needs them. getQueryTriggerProps(query) binds
focus-preserving preset buttons.
useCombobox() compatibility
useCombobox() is deprecated. It remains available when a form-like selected
value is required: it retains selectedItem, selectedValue, getItemValue(),
and the legacy select-on-activation lifecycle. Prefer useQuery() for new
query surfaces and keep selection or submitted values in application state.
Guarding close requests
Pass onBeforeClose through FloatingRoot options to synchronously perform a
final check or send information. Return false to keep the surface open.
<FloatingRoot
v-model:open="open"
:options="{
onBeforeClose: (event, reason) => {
reportClose(reason);
return canClose();
},
}"
>
<!-- reference and content -->
</FloatingRoot>
The same callback covers FloatingClose, dismiss() interactions, and native
Popover/dialog dismissal. It is synchronous: finish async saving or
confirmation first, then set open.value = false.
Main API
| API | Use it for |
|---|---|
useFloating() / v-floating |
Renderless positioning with application-owned elements |
FloatingRoot, FloatingReference, FloatingContent |
Vue-native surface composition |
FloatingPortal, FloatingOverlay, FloatingFocusManager |
Teleport and custom modal composition |
FloatingList, FloatingListItem, FloatingTree |
Keyboard collections and nested menus |
useSearch(), FloatingResults, useQuery() |
Search lifecycle, result rendering, and query interaction |
click, hover, focus, dismiss, role |
Composable interaction plugins |
FloatingResults is the preferred phase renderer. FloatingSearch remains as
a deprecated compatibility export, so existing templates continue to work.
Documentation
- Home · Getting started · Vue guide
- Usage recipes · Combobox demos · Dismiss and closing
- Entry and exit animation
Verify
bun run --filter '@floating-ui-plus/vue' typecheck
bun run --filter '@floating-ui-plus/vue' test
bun run --filter '@floating-ui-plus/vue' test:browser
Related Packages
Overlay components (Modal, Popover, Tooltip, DropdownMenu, ContextMenu) for vuec...
Idiomatic Vue popover/tooltip wrapping @floating-ui/dom — one Rozie source compi...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.10.1 | >=3.3.0 | MIT | |
| 0.10.0 | >=3.3.0 | MIT | |
| 0.9.0 | >=3.3.0 | MIT | |
| 0.8.2 | >=3.3.0 | MIT | |
| 0.8.1 | >=3.3.0 | MIT | |
| 0.7.3 | >=3.3.0 | MIT | |
| 0.7.2 | >=3.3.0 | MIT | |
| 0.6.0 | >=3.3.0 | MIT | |
| 0.5.0 | >=3.3.0 | MIT | |
| 0.4.0 | >=3.3.0 | MIT | |
| 0.3.0 | >=3.3.0 | MIT | |
| 0.2.2 | >=3.3.0 | MIT | |
| 0.2.1 | >=3.3.0 | MIT | |
| 0.2.0 | >=3.3.0 | MIT |