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:
autoPosition: false is provided)$state for .referenced, .tethered, .attached (tethered ?? referenced)portal actionoverlay action with optional scroll lockingcreateSingleton factory for singleton UI patternsOther than that, just use it as you would use @floating-ui🎉
Happy coding!🦒
Usage
Options and properties
bun add floating-runes
use:float - Designating the floating elementsuse:float.arrow - Designated arrow element; must be a direct child element of use:floatuse:float.ref - The thing the floated element(s) is referencingSvelte 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 multipleuse:floatfrom 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:
float.virtual({ getBoundingClientRect: () => ... })float.virtual(() => ({ x, y })) (updates when dependencies change)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]
Thearrowmiddleware does not take anelementproperty. Instead apply the Svelte actionuse:float.arrow
.then(...)Read more aboutconst float = floatingUI(...).then((data: ComputePositionReturn) => void)
float.referenced, float.tethered and float.attachedThe 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 |
float.arrowuse: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 thearrowmiddleware, 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)float.ref and float.tetheruse: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 and float.untetherfloat.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'}
float.virtual and float.unvirtualSet 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}
float.placementReactive 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)VirtualElement type (re-export from @floating-ui/dom)createSingletonCreate 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 |