vue-afloat
vue-afloat
Tooltips, dropdowns and menus for Vue, powered by Floating UI.
A slimmed-down port of floating-vue, with modern build, CSS-vars-based styling system, ESM-only and Composition API.
Install
pnpm add vue-afloat
Usage
Register the plugin and import the stylesheet once:
import { createApp } from 'vue'
import FloatingVue from 'vue-afloat'
import App from './App.vue'
import 'vue-afloat/style.css'
createApp(App).use(FloatingVue).mount('#app')
Tooltip directive
<template>
<button v-tooltip="'Hello tooltip'">
Hover me
</button>
<!-- placement via modifier, and HTML content -->
<button v-tooltip.bottom="{ content: '<b>Rich</b> tooltip', html: true }">
Hover me
</button>
</template>
Components
VTooltip, VDropdown and VMenu share the same shape: a default slot for the
trigger and a #popper slot for the floating content. Use v-close-popper
inside a dropdown to close it.
<template>
<VDropdown>
<button>Open</button>
<template #popper>
<MyMenu />
<button v-close-popper>
Close
</button>
</template>
</VDropdown>
<VMenu placement="right-start">
<button>File</button>
<template #popper>
<VMenu placement="right-start">
<button>Export ›</button>
<template #popper>
…
</template>
</VMenu>
</template>
</VMenu>
</template>
Shadow DOM and custom elements
Inside an open shadow root, components and v-tooltip keep their floating
content in that root by default. Include the stylesheet in the shadow root,
since a document-level import cannot style its contents. With Vue's
defineCustomElement and Vite:
import { defineCustomElement, h } from 'vue'
import { Dropdown } from 'vue-afloat'
import styles from 'vue-afloat/style.css?inline'
const MyDropdown = defineCustomElement({
styles: [styles],
setup() {
return () => h(Dropdown, {}, {
default: () => h('button', 'Open'),
popper: () => h('div', 'Content inside the shadow root'),
})
},
})
customElements.define('my-dropdown', MyDropdown)
The stylesheet defines defaults on :host as well as :root. Add your own
:host rules after it, or set --vue-afloat-* properties on the custom element,
to customize each instance without injecting styles into the document.
The default container: 'body' uses the reference element's shadow root when
present, otherwise the document body. Other container selectors resolve only
within that root. You can also pass an HTMLElement or ShadowRoot directly,
or use container: false to keep content beside its trigger. Passing
document.body explicitly moves content outside the shadow root and requires
document-level styles.
Styling
Tooltip, dropdown and menu share one default look that adapts to light/dark via
prefers-color-scheme. Import the stylesheet and override the --vue-afloat-*
custom properties globally, per type, or per instance. No theme registry.
/* global */
:root {
--vue-afloat-border-radius: 12px;
}
/* per type: .vue-afloat-{tooltip,dropdown,menu} */
.vue-afloat-tooltip {
--vue-afloat-background: #18181b;
--vue-afloat-color: #fff;
}
/* per instance: pass popper-class="glass" */
.vue-afloat-tooltip.glass {
--vue-afloat-background: rgb(255 255 255 / 0.55);
--vue-afloat-backdrop-blur: 12px;
}
CSS classes
| Class | Applied to | Purpose |
|---|---|---|
.vue-afloat |
reference wrapper, floating popper | base block |
.vue-afloat--shown / .vue-afloat--hidden |
reference wrapper, floating popper | current visibility state |
.vue-afloat--show-from / .vue-afloat--show-to |
floating popper | present while the show transition runs |
.vue-afloat--hide-from / .vue-afloat--hide-to |
floating popper | present while the hide transition runs |
.vue-afloat--skip-transition |
floating popper | disables the opacity/visibility transition |
.vue-afloat--top / --bottom / --left / --right |
floating popper | resolved placement side (replaces [data-popper-placement]) |
.vue-afloat--no-arrow |
floating popper | applied automatically by the no-arrow prop; hides the arrow |
.vue-afloat--has-tooltip |
trigger element (v-tooltip) |
marks an element with an attached tooltip |
.vue-afloat--tooltip-loading |
floating popper (VTooltip/v-tooltip only) |
present while async tooltip content is loading |
.vue-afloat--some-open |
<body> or shadow host |
present while at least one popper in that root is open |
.vue-afloat-tooltip / .vue-afloat-dropdown / .vue-afloat-menu |
floating popper | per-component hook, override tokens here to diverge per type |
.vue-afloat__backdrop |
floating popper (child) | full-size click-catcher used by autoHide |
.vue-afloat__wrapper |
floating popper (child) | wraps the content and the arrow |
.vue-afloat__inner |
floating popper (child) | scrollable box holding your slot content |
.vue-afloat__arrow-container |
floating popper (child) | positions the arrow |
.vue-afloat__arrow-inner / .vue-afloat__arrow-outer |
floating popper (child) | arrow fill and outline layers |
CSS variables
| Variable | Default (light / dark) | Used for |
|---|---|---|
--vue-afloat-background |
#fff / #1e1e24 |
content background, and the arrow fill fallback |
--vue-afloat-color |
#1f2933 / rgb(255 255 255 / 0.92) |
text color |
--vue-afloat-border-color |
rgb(0 0 0 / 0.1) / rgb(255 255 255 / 0.14) |
border, and the arrow outline fallback |
--vue-afloat-border-width |
1px |
content border width |
--vue-afloat-border-radius |
8px |
content corner radius |
--vue-afloat-box-shadow |
0 4px 24px rgb(0 0 0 / 0.12) / 0 4px 24px rgb(0 0 0 / 0.5) |
drop shadow |
--vue-afloat-padding |
6px 10px |
content padding |
--vue-afloat-backdrop-blur |
0px |
backdrop-filter: blur() on the content |
--vue-afloat-arrow-size |
6px |
arrow border-width |
--vue-afloat-arrow-color |
unset (falls back to --vue-afloat-background) |
arrow fill, set to diverge from the content background |
--vue-afloat-arrow-border-color |
unset (falls back to --vue-afloat-border-color) |
arrow outline, set to diverge from the content border |
--vue-afloat-z-index |
10000 |
floating popper z-index |
--vue-afloat-transition-duration |
0.15s |
show/hide transition duration |
Global config
Pass defaults to the plugin, or mutate the exported options object:
app.use(FloatingVue, {
distance: 8,
disposeTimeout: 150,
autoHideOnMousedown: false,
})
Options reference
| Option | Default | Description |
|---|---|---|
disabled |
false |
Disable all poppers |
placement |
'bottom' |
Preferred placement relative to the reference element |
distance |
5 |
Position offset along the main axis (px) |
skidding |
0 |
Position offset along the cross axis (px) |
container |
'body' |
Container element, shadow root, root-scoped selector, or false for the trigger's parent. The default uses the reference's shadow root when present. |
boundary |
undefined |
Element used to compute position and size boundaries |
instantMove |
false |
Skip delay & CSS transitions when moving from one popper to another |
disposeTimeout |
150 |
Auto destroy popper DOM nodes after hiding (ms) |
popperTriggers |
[] |
Triggers on the popper itself |
strategy |
'absolute' |
Positioning strategy ('absolute' or 'fixed') |
preventOverflow |
true |
Prevent overflow |
flip |
true |
Flip to the opposite placement if needed |
shift |
true |
Shift on the cross axis to prevent overflow |
shiftCrossAxis |
false |
Also shift on the cross axis (perpendicular to the placement) |
overflowPadding |
0 |
Overflow padding (px) |
arrowPadding |
0 |
Arrow padding (px) |
noArrow |
false |
Hide the arrow |
autoHideOnMousedown |
false |
Compute auto hide on mousedown instead of click |
API
- Components:
Tooltip(VTooltip),Dropdown(VDropdown),Menu(VMenu) - Directives:
vTooltip,vClosePopper - Directive helpers:
createTooltip,destroyTooltip - Low-level:
createPopper(renderless factory) +PopperContentfor custom popovers,createAfloatComponentfor building your ownTooltip/Dropdown/Menu-shaped component - Globals:
hideAllPoppers,recomputeAllPoppers,placements,options - Types:
Placement,TriggerEvent,FloatingVueConfig
Creating your own component
Tooltip, Dropdown and Menu are all built with createAfloatComponent, which
bakes in default props and a CSS class for a createPopper + PopperContent
pair. Use it to define your own variant with different defaults:
import { createAfloatComponent } from 'vue-afloat'
const VDropdown = createAfloatComponent('VDropdown', 'vue-afloat-dropdown', {
placement: 'bottom',
triggers: () => ['click'],
delay: 0,
handleResize: true,
autoHide: true,
})
The second argument is the class applied to the floating popper (see CSS classes); the third is a map of prop defaults, applied on top of the standard popper props.
Migrate from floating-vue
The main usage (the plugin, the three components, v-tooltip and
v-close-popper) is unchanged. The differences:
- Imports:
floating-vue→vue-afloat, andfloating-vue/dist/style.css→vue-afloat/style.css. - CSS classes renamed:
v-popper*→vue-afloat*throughout (e.g.v-popper__inner→vue-afloat__inner,v-popper--theme-tooltip→vue-afloat-tooltip). See the CSS classes table above for the full mapping. - Theme system removed: there is no
config.themes, nothemeprop, and nogetThemeClasses. The built-in components keep their look through the same.vue-afloat-*classes; customise with the--vue-afloat-*CSS variables instead of registering themes. - Deprecated APIs removed: use
vTooltip/vClosePopper(not theVTooltip/VClosePopperconstants); thePopperMethods/ThemeClassmixins are gone (use the Composition API);autoMinSize→autoSize="min"andautoMaxSize→autoBoundaryMaxSize. - Dropped: the Nuxt module, the UMD build (ESM-only now), the
SHOW_EVENT_MAP/HIDE_EVENT_MAPexports, and the rarely-used propsautoSize,autoBoundaryMaxSize,arrowOverflow,computeTransformOrigin,showGroupandpositioningDisabled.
Credits
Forked from floating-vue by Guillaume Chau. All credit for the original design and API goes to the floating-vue authors and contributors.
License
MIT License © Anthony Fu MIT License © Guillaume Chau
Related Packages
Version History
| Version | Released | License |
|---|---|---|
| 0.2.0 | MIT | |
| 0.1.1 | MIT | |
| 0.1.0 | MIT | |
| 0.0.0 | MIT |