@floating-ui-plus/vue

Vue Floating UI composables and components for interactions, focus management, portals, collections, and search
2,183
Install
npm install @floating-ui-plus/vue
Latest Version:0.10.1
Vue:3
License:MIT
Last Updated:Sep 27, 2026
Links: npm
Maintainer: polcaneli

@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

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

@floating-ui/vue

Floating UI for Vue

106,918,631 32,773
vue-afloat

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

1,676
v-float

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

2,353 7
@vuecs/overlays

Overlay components (Modal, Popover, Tooltip, DropdownMenu, ContextMenu) for vuec...

6,801 4
@rozie-ui/popover-vue

Idiomatic Vue popover/tooltip wrapping @floating-ui/dom — one Rozie source compi...

2,266 1

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