floating-runes Svelte Themes

Floating Runes

Uses Svelte 5 runes to wrap floating-ui with additional features like tethering and conditional triggers

Floating Runes

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:

Other than that, just use it as you would use @floating-ui🎉

Happy coding!🦒


Usage

  1. Simple example
  2. Tethering
  3. Virtual positioning
  4. Advanced use
  5. Portal action
  6. Overlay action
  7. Singleton pattern

Options and properties

  1. floatingUI
  2. use:float
  3. float.ref and float.tether
  4. float.unref and float.untether
  5. float.virtual and float.unvirtual
  6. float.placement
  7. Additional exports
  8. createSingleton

Usage

bun add floating-runes

  • use:float - Designating the floating elements
  • use:float.arrow - Designated arrow element; must be a direct child element of use:float
  • use: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.


Tethering

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>

Virtual positioning

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>

Advanced

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>

Bonus

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>

Overlay action

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>


Options

floatingUI

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

.then(...)

Read more about
const float = floatingUI(...).then((data: ComputePositionReturn) => void)

float.referenced, float.tethered and float.attached

The element that has been referenced to, or tethered to. Attached will return tethered ?? referenced


float

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

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)

float.ref and float.tether

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 and float.untether

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'}


float.virtual and float.unvirtual

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}

float.placement

Reactive getter for the computed placement (updates on flip/shift).

<div use:float>Tooltip</div>
<span>Placement: {float.placement}</span>

Additional exports

  • detectOverflow (re-export from @floating-ui/dom)
  • VirtualElement type (re-export from @floating-ui/dom)

createSingleton

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' })

Singleton Pattern

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>

Programmatic control

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.

Singleton options

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']

Singleton API

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


Top categories

Loading Svelte Themes