npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@floating-ui-plus/vue

v0.11.2

Published

Vue Floating UI composables and components for interactions, focus management, portals, collections, and search

Readme

@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.

Editor-owned queries

useQuery() also exposes bindExternalInput(element), setExternalQuery(text, {composing, event?}), and handleKeyDown(event). Bind a contenteditable focus target and supply the query from editor updates; do not bind inputProps to the same external editor. The controller installs no input/composition/keyboard listeners and does not replace document text.

Use query.controller.navigationPlugin({keyboardHandling: 'manual', loop: true}) as a floating interaction. In the editor's keyboard hook, after its IME checks, return navigation.handleKeyDown(event) || query.handleKeyDown(event). The navigation plugin must be connected to the floating controller. Keep the editor as its DOM reference and use a virtual position reference for the caret. Invoke the unbind callback when changing editor targets; scope disposal also cleans up the controller's current binding.

See the editor Query guide for the shared contract, manual navigation, and editor-owned validation.

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