Web Components and Vue
The Web Components and Vue packages expose the same interaction model. Choose the tab that matches your renderer; the close contract, native top-layer rules, and search lifecycle remain the same. Detailed component-family guidance lives in the Components section.
One contract, two renderers
Section titled “One contract, two renderers”Both adapters provide a reference, a floating surface, an open model, interaction plugins, and an optional focus manager. The application owns labels, classes, ARIA names, transition styling, and result markup; the adapter owns bindings, lifecycle state, and the native surface integration.
When you opt into a native surface, Floating UI Plus uses the browser’s Web
Standards APIs: the Popover API
for anchored non-modal surfaces and the native <dialog> element
for modal surfaces.
Register the elements once, then compose a root-owned template:
<floating-root placement="bottom-start" interactions="click dismiss"> <floating-reference> <button type="button">Open settings</button> </floating-reference> <template slot="content"> <section aria-label="Settings"> Settings <button type="button" data-fup-close>Close</button> </section> </template></floating-root>import "@floating-ui-plus/web-components";import { flip, offset, shift } from "@floating-ui-plus/web";import type { FloatingOpenChangeDetail, FloatingRootElement,} from "@floating-ui-plus/web-components";
const root = document.querySelector<FloatingRootElement>("floating-root")!;root.middleware = [offset(8), flip(), shift({ padding: 12 })];root.addEventListener("openchange", (event) => { const { detail } = event as CustomEvent<FloatingOpenChangeDetail>; console.log(detail.open, detail.reason, detail.sourceEvent);});Use a JavaScript property for functions and objects such as middleware, plugins, search sources, or virtual references. Attributes are for strings and booleans.
Use the component layer when descendants should receive the controller:
<script setup lang="ts">import { ref } from "vue";import { FloatingClose, FloatingContent, FloatingReference, FloatingRoot, click, dismiss, offset,} from "@floating-ui-plus/vue";
const open = ref(false);const plugins = [click(), dismiss()];const options = { placement: "bottom-start", middleware: [offset(8)] };</script>
<template> <FloatingRoot v-model:open="open" :options="options" :plugins="plugins"> <FloatingReference>Open settings</FloatingReference> <FloatingContent> Settings <FloatingClose>Close</FloatingClose> </FloatingContent> </FloatingRoot></template>Use useFloating() instead when the component already owns its reference and
panel refs. It exposes the same controller context as FloatingRoot, without
requiring the declarative child components.
Every floating surface starts with dialog semantics. Use floating-role or
role() only when a surface needs a different ARIA contract, such as menu,
tooltip, or combobox; they do not choose a native top-layer surface. Web
Components root-owned templates and ordinary slotted
surfaces, as well as Vue FloatingContent, use the Popover API by default.
Use top-layer="none" for an intentionally positioned/custom surface and
as="dialog" for a native dialog.
Native top-layer surfaces
Section titled “Native top-layer surfaces”For an anchored non-modal surface, keep the content in the same tree and let the browser use Popover. For a modal, use a real dialog element:
<floating-root interactions="click dismiss"> <floating-reference><button>Open account</button></floating-reference> <dialog slot="floating" aria-labelledby="account-title"> <h2 id="account-title">Account settings</h2> <button data-fup-close>Close</button> </dialog></floating-root><FloatingRoot v-model:open="open" :plugins="[click(), dismiss()]"> <FloatingReference>Open account</FloatingReference> <FloatingContent as="dialog" aria-labelledby="account-title"> <h2 id="account-title">Account settings</h2> <FloatingClose>Close</FloatingClose> </FloatingContent></FloatingRoot>Do not portal the same native surface. Add floating-portal or
FloatingPortal only when the element must move to a body-level target, such
as escaping a clipping ancestor. For a custom non-native modal, compose a
portal with an overlay and focus manager.
Close guards and dismissal
Section titled “Close guards and dismissal”dismiss() detects outside press, Escape, reference press, and ancestor
scroll. It is not the close API. close()/FloatingClose are explicit close
commands, and every close path runs the same synchronous before-close guard.
root.addEventListener("floatingbeforeclose", (event) => { if (hasUnsavedChanges()) { event.preventDefault(); return; } saveCloseMetric(event.detail.reason);});
// Imperative close; keep the source event when one exists.root.close(clickEvent, "click");preventDefault() keeps open unchanged and suppresses openchange.
<FloatingRoot v-model:open="open" :options="{ onBeforeClose: (event, reason) => { saveCloseMetric(reason); return !hasUnsavedChanges(); }, }"> <FloatingReference>Open</FloatingReference> <FloatingContent> Unsaved settings <FloatingClose>Close</FloatingClose> </FloatingContent></FloatingRoot>Return false to cancel. Async approval is not part of this contract: finish
the async operation first, then set the open model to false or call the
imperative close method.
Search and query
Section titled “Search and query”The Web Components and Vue search adapters consume the same phases:
idle, loading, error, empty, and results. Use fuzzy search for a
local collection and an async source for a server-backed list. The controller
owns debounce, cancellation, stale-response protection, de-duplication, and
cursor pagination; each renderer owns the phase markup.
<floating-root placement="bottom-start"> <floating-list navigation loop allow-escape> <floating-query> <floating-reference> <input aria-label="Destination" /> </floating-reference> <template slot="content"> <floating-results> <floating-results-status type="loading">Searching…</floating-results-status> <floating-results-status type="empty">No matches</floating-results-status> <floating-results-item> <floating-list-item> <button type="button" data-search-text="label"></button> </floating-list-item> </floating-results-item> </floating-results> </template> </floating-query> </floating-list></floating-root>Configure search, getItemLabel, and getItemKey as properties in
JavaScript. The default semantics are ARIA combobox; use semantics="dialog"
for a command palette or semantics="none" when the renderer owns ARIA.
<FloatingRoot v-model:open="query.open" :plugins="[dismiss(), query.rolePlugin]"> <FloatingList v-model:active-index="query.activeIndex" navigation loop :navigation-options="query.getNavigationOptions({allowEscape: true})" > <FloatingReference as="input" v-bind="query.inputProps" /> <FloatingContent> <FloatingResults :search="search"> <template #loading>Searching…</template> <template #empty>No matches</template> <template #results> <FloatingListItem v-for="(item, index) in search.items.value" :key="item.id" tag="button" :label="item.label" :value="item" v-bind="query.getOptionProps(item, index)" >{{ item.label }}</FloatingListItem> </template> </FloatingResults> </FloatingContent> </FloatingList></FloatingRoot>Use useSearch() and useQuery() to create the reactive search,
inputProps, and getOptionProps values. useCombobox() remains available
for the specialized selected-value workflow.
For a full local fuzzy source implementation, see the Combobox fuzzy-search demo, and for an application-owned async source see the server-search demo. For complete composition examples, see the usage recipes.