Svelte 5 Runes powered wrapper for @floating-ui. An alternative approach to svelte-floating-ui which approx. does the same thing.
floating-runes will also:
- Position arrow/floater automatically (unless
autoPosition: falseis provided) - Access the elements via
$statefor.referenced,.tethered,.attached(tethered ?? referenced) - Tethering (temporary element reference)
- Virtual positioning for cursor/selection-based UIs
- Conditional reference/tethering
- A
portalaction - An
overlayaction with optional scroll locking - A
createSingletonfactory for singleton UI patterns
Other than that, just use it as you would use @floating-ui🎉
Happy coding!🦒
Usage
- Simple example
- Tethering
- Virtual positioning
- Advanced use
- Portal action
- Overlay action
- Singleton pattern
Options and properties
- floatingUI
- use:float
- float.ref and float.tether
- float.unref and float.untether
- float.virtual and float.unvirtual
- float.placement
- Additional exports
- createSingleton
bun add floating-runes
use:float- Designating the floating elementsuse:float.arrow- Designated arrow element; must be a direct child element ofuse:floatuse:float.ref- The thing the floated element(s) is referencing
Svelte Playground - Usage example
<script>
import floatingUI, { flip, shift, arrow } from 'floating-runes'
const float = floatingUI({
placement: 'top',
middleware: [
flip(),
shift(),
arrow()
]
})
</script>
<div>
<tooltip use:float>
<arrow use:float.arrow></arrow>
</tooltip>
<button use:float.ref> Hover me </button>
</div>Tip
P.S. you can use multiple use:float from the same declaration.
You can use float.tether(element) to float to another element than the float.ref. Then use float.untether() and it returns to float.ref.
Svelte Playground - Tethering example
<script>
import floatingUI, { flip, shift, arrow } from 'floating-runes'
let url = '/a' // demo example
const float = floatingUI()
</script>
{#snippet href(ref, text)}
<a
class:active={ref === url}
use:float.ref={() => ref === url}
use:float.tether={'pointerenter'}
href={ref}
>
{text}
</a>
{/snippet}
{#if float.tethered}
<div class='hovered' use:float={{ untether: false }}></div>
{/if}
<div class='active' use:float={{ tether: false }}></div>
<div use:float.untether={'pointerleave'}>
{@render href('/a', 'Hover me')}
{@render href('/b', 'I want attention')}
{@render href('/c', 'Guys... what about meeEeEe')}
{@render href('/d', 'Ignore my brother')}
</div>Position relative to a virtual point (mouse cursor, selection range, etc.).
float.virtual(...) supports:
- VirtualElement:
float.virtual({ getBoundingClientRect: () => ... }) - Reactive getter:
float.virtual(() => ({ x, y }))(updates when dependencies change) - Action + event:
use:float.virtual={'pointermove'}(tracks the pointer)
float.unvirtual(...) clears the virtual reference and can also be event-driven.
<script>
import floatingUI from 'floating-runes'
const float = floatingUI({ strategy: 'fixed' })
</script>
<div
use:float.virtual={'pointermove'}
use:float.unvirtual={'pointerleave'}
>
Hover me
</div>
{#if float.referenced}
<div class='tooltip' use:float>Follows the cursor</div>
{/if}For context menus, you can also set a custom virtual element based on the event:
<div oncontextmenu={(e) => {
float.virtual({
getBoundingClientRect: () => ({
width: 0,
height: 0,
x: e.clientX,
y: e.clientY,
top: e.clientY,
left: e.clientX,
right: e.clientX,
bottom: e.clientY
})
})
}}>
Right click me
</div>As per the documentation of @floating-ui, you can access the .then(...) which works in the same way as their documentation.
So you can go wild🦒
<script>
import floatingUI, { ... } from 'floating-runes'
const float = floatingUI({
placement: 'top',
middleware: [
...
]
}).then(computedData => {
const { middlewareData } = computedData
...
})
</script>As a bonus, you can use portal to move an element to another (such as the body).
When the component is destroyed, the element that was portalled, will naturally, also get destroyed.
<script>
import { portal } from 'floating-runes'
</script>
<div use:portal> I'm in the body😏 </div>
<div use:portal={element}> I'm in another element </div>Create a full-screen backdrop and optionally lock body scroll (default: true).
<script>
import { overlay } from 'floating-runes'
</script>
<div class='backdrop' use:overlay></div>Disable scroll locking:
<div class='backdrop' use:overlay={{ lockScroll: false }}></div>FloatingRuneOptions extends ComputePositionConfig
| Property | Type | Description |
|---|---|---|
| middleware? | Middleware[] | Array of middleware objects to modify the positioning or provide data for rendering |
| platform? | Platform | Custom or extended platform object |
| placement? | | 'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' |
Where to place the floating element relative to its reference element Default: 'bottom' |
| strategy? | 'absolute' | 'fixed' |
The type of CSS position property to use Default: absolute |
| autoUpdate? | AutoUpdateOptions | Whether or not to auto-update the floating element's position |
| autoPosition? | boolean |
Whether or not to auto-position the floating element and the arrow, and auto-assign the position: to the strategy (absolute/fixed)Default: true |
Note
The arrow middleware does not take an element property. Instead apply the Svelte action use:float.arrow
Read more aboutconst float = floatingUI(...).then((data: ComputePositionReturn) => void)
The element that has been referenced to, or tethered to. Attached will return tethered ?? referenced
use:float
This Svelte action creates a floater, that floats relative to the reference- and tethered element.
use:float={FloatOptions}
| Property | Type | Description |
|---|---|---|
tether |
boolean |
Whether-to-tether. Default: true |
untether |
boolean |
If false it will stick to the last tethered target, instead of going back to the reference. Default: true |
use:float.arrow
This Svelte action creates reference to the element that serves as the arrow to a use:float element. Must be a direct child.
<div use:float>
...
<arrow use:float.arrow>...</arrow>
</div>Tip
Remember to include the arrow middleware,
and put it after other middlewares if needed.
The arrow element also receives CSS custom properties that reflect the computed placement:
--float-side:top | right | bottom | left--float-rotation:0deg | 90deg | 180deg | 270deg--float-placement: full placement string (e.g.top-start)
use:float.ref and use:float.tether
These Svelte actions sets the reference point for the use:float element.
Additionally, they accept a trigger parameter: A conditional callback (() => boolean) or an event (keyof WindowEventMap).
Ex.
use:float.ref={() => url === href}
or
use:float.tether={'pointerenter'}
float.unref removes the current reference.
float.untether removes the tethering, so that the floating element will return to the reference (unless untether: false is provided).
Both can be used directly either via float.unref() / float.untether()
Or like float.ref and float.tether have a condition to trigger;
Ex.
use:float.untether={() => condition}
or
use:float.unref={'pointerleave'}
Set or clear a virtual reference. VirtualElement is re-exported from @floating-ui/dom.
<!-- Action style (event-driven) -->
<div use:float.virtual={'pointermove'} use:float.unvirtual={'pointerleave'}>
Follow the cursor
</div><!-- Reactive getter style -->
<script>
import floatingUI from 'floating-runes'
const float = floatingUI({ strategy: 'fixed' })
let x = $state(0)
let y = $state(0)
float.virtual(() => ({ x, y }))
</script>
<div onpointermove={(e) => { x = e.clientX; y = e.clientY }}>
Track
</div>
{#if float.referenced}
<div use:float>Cursor: {x}, {y}</div>
{/if}Reactive getter for the computed placement (updates on flip/shift).
<div use:float>Tooltip</div>
<span>Placement: {float.placement}</span>detectOverflow(re-export from@floating-ui/dom)VirtualElementtype (re-export from@floating-ui/dom)
Create a callable singleton action that can be exported from <script module> and shared across the app.
import { createSingleton } from 'floating-runes'
const tooltip = createSingleton({ placement: 'top' })For tooltips, context menus, and dropdowns where only one should be visible at a time, use createSingleton.
The singleton should be created in a <script module> so the instance is shared across all imports:
<!-- TooltipRoot.svelte -->
<script module lang='ts'>
import { createSingleton, offset, flip, shift, arrow } from 'floating-runes'
import type { Snippet } from 'svelte'
export const tooltip = createSingleton<string | Snippet>({
placement: 'top',
strategy: 'fixed',
middleware: [
offset(8),
flip({ padding: 8 }),
shift({ padding: 8 }),
arrow()
],
showDelay: 200,
hideDelay: 0
})
</script>
<script lang='ts'>
import { portal } from 'floating-runes'
</script>
{#if tooltip.visible && tooltip.content !== undefined}
<div class='tooltip-container' use:tooltip.float use:portal>
<div class='tooltip'>
{#if typeof tooltip.content === 'string'}
{tooltip.content}
{:else}
{@render tooltip.content()}
{/if}
</div>
<div class='tooltip-arrow' use:tooltip.arrow></div>
</div>
{/if}Use it anywhere by importing the module export:
<script>
import TooltipRoot, { tooltip } from './TooltipRoot.svelte'
</script>
<TooltipRoot />
<button use:tooltip={'Helpful tooltip text'}>Hover me</button>
<button use:tooltip={{ content: 'Custom delay', showDelay: 500 }}>Hover me</button>tooltip.show('Hello', buttonEl)
tooltip.show('At cursor', { x: event.clientX, y: event.clientY })
tooltip.hide()For context menus, combine singleton usage with virtual positioning.
| Option | Type | Description |
|---|---|---|
middleware? |
Middleware[] |
Floating UI middleware array Default: [offset(8), flip(), shift(), arrow()] |
showDelay? |
number |
Delay before showing (ms) Default: 0 |
hideDelay? |
number |
Delay before hiding (ms) Default: 0 |
showOn? |
TriggerEvent | TriggerEvent[] |
Events that show the singleton Default: ['pointerenter', 'focus'] |
hideOn? |
TriggerEvent | TriggerEvent[] |
Events that hide the singleton Default: ['pointerleave', 'blur'] |
| Property | Type | Description |
|---|---|---|
visible |
boolean |
Whether the floating element is currently visible |
content |
string | Snippet |
Current content to display |
anchor |
HTMLElement |
Current anchor element (DOM triggers only) |
placement |
Placement |
Computed placement after positioning |
float |
Action |
Action to apply to the floating element |
arrow |
Action |
Action to apply to the arrow element |
show(content, anchor?) |
function |
Programmatically show, optionally at an element or { x, y } |
hide() |
function |
Programmatically hide |