Svelte attachment for floating-ui. Position floating elements like tooltips, popovers, and dropdowns with automatic reactivity - most modern floating-ui wrapper for svelte.
npm install svelte-floating-attach @floating-ui/dom
@floating-ui/dom is a peer dependency, not bundled into this package. You need to install it alongside to prevent version conflicts.
>=5.29.0 (attachment support)@floating-ui/dom >=1.6.0This library is heavily inspired by svelte-floating-ui. If you're on ≤ Svelte 5.29 you should use that library instead.
svelte-floating-attach requires ≥ Svelte 5.29. It uses attachments ({@attach}) API. Attachments work better than actions.
svelte-floating-ui?svelte-floating-attach |
svelte-floating-ui |
|
|---|---|---|
| Reactivity | Automatic — attachment re-runs when any $state in its arguments changes |
Manual (if using runes) — requires $effect + update() to sync prop changes |
| Arrow/caret positioning | Automatic — left/top styles applied to the arrow element after each computation |
Manual — you read middlewareData.arrow in onComputed and apply styles yourself |
@floating-ui/dom |
Peer dependency — installed separately, no duplicates | Direct dependency — bundled in, may duplicate if you also depend on it directly |
| Svelte stores | None — plain closures | Uses writable stores for arrow refs and virtual elements |
| Bundle footprint | ~5 KB compiled (re-exports from your existing @floating-ui/dom) |
Bundles its own copy of @floating-ui/dom into the package |
Actions (use:) don't re-run when their arguments change. With svelte-floating-ui, you need a manual $effect to push updated options:
<script>
import { createFloatingActions } from 'svelte-floating-ui'
import { flip, offset, shift } from 'svelte-floating-ui/dom'
let { placement = $bindable('bottom') } = $props()
const [floatingRef, floatingContent, updatePosition] = createFloatingActions({
placement,
middleware: [offset(8), shift(), flip()],
})
// Required: manually sync reactive props to the action
$effect(() => {
updatePosition({ placement })
})
</script>
<button use:floatingRef>Trigger</button>
<div use:floatingContent>Content</div>
Attachments run in the template's reactive tracking context — when placement changes, the attachment tears down and re-runs with the new value. No $effect, no update():
<script>
import { createFloating, flip, offset, shift } from 'svelte-floating-attach'
let { placement = $bindable('bottom') } = $props()
const { ref, content } = createFloating()
</script>
<button {@attach ref}>Trigger</button>
<div {@attach content({
placement,
middleware: [offset(8), shift(), flip()],
})}>
Content
</div>
In svelte-floating-ui, you create a writable store for the arrow element and manually apply its computed position inside onComputed:
<script>
import { createFloatingActions, arrow } from 'svelte-floating-ui'
import { createArrowRef } from 'svelte-floating-ui'
const arrowRef = createArrowRef()
const [floatingRef, floatingContent] = createFloatingActions({
middleware: [arrow({ element: arrowRef })],
onComputed({ placement, middlewareData }) {
// You must manually read and apply arrow styles
const { x, y } = middlewareData.arrow
const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[
placement.split('-')[0]
]
Object.assign($arrowRef.style, {
left: x != null ? `${x}px` : '',
top: y != null ? `${y}px` : '',
[staticSide]: '-4px',
})
},
})
</script>
<div use:floatingContent>
Content
<div bind:this={$arrowRef} class="arrow"></div>
</div>
In svelte-floating-attach, this is built in. The library automatically applies left/top from middlewareData.arrow to the arrow element after every computation, and resets stale right/bottom when placement changes:
<script>
import { createFloating, offset, flip, shift } from 'svelte-floating-attach'
const { ref, content, arrow, arrowMiddleware } = createFloating()
</script>
<div {@attach content({
placement: 'top',
middleware: [offset(8), flip(), shift(), arrowMiddleware({ padding: 4 })],
})}>
Content
<div {@attach arrow} class="arrow"></div>
</div>
<script>
import { createFloating, offset, flip, shift } from 'svelte-floating-attach'
let show = $state(false)
const { ref, content } = createFloating()
</script>
<button {@attach ref} onclick={() => show = !show}>
Toggle
</button>
{#if show}
<div {@attach content({
placement: 'bottom',
middleware: [offset(8), flip(), shift()],
})}>
Popover content
</div>
{/if}
A complete example showing how placement reacts to prop changes and how onComputed keeps the actual placement in sync when Floating UI flips it due to lack of space.
<!-- Popover.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte'
import type { Placement } from 'svelte-floating-attach'
import { createFloating, offset, flip, shift, hide } from 'svelte-floating-attach'
interface Props {
/** Preferred placement — may be overridden by flip() */
placement?: Placement
show?: boolean
children?: Snippet
content?: Snippet
}
let {
placement = $bindable('bottom'),
show = $bindable(false),
children,
content: contentSnippet,
}: Props = $props()
const { ref, content: floatingContent } = createFloating()
</script>
<button {@attach ref} onclick={() => show = !show}>
{@render children?.()}
</button>
{#if show}
<div {@attach floatingContent({
placement,
middleware: [offset(8), shift(), flip(), hide()],
onComputed: (data) => (placement = data.placement),
})}>
{@render contentSnippet?.()}
</div>
{/if}
When the consumer passes a different placement prop, the attachment re-runs automatically — no $effect needed. The onComputed callback writes back the actual placement so the consumer always knows where the popover ended up (e.g., 'top' instead of 'bottom' after a flip).
<!-- Consumer.svelte -->
<script>
import Popover from './Popover.svelte'
let placement = $state('bottom')
</script>
<select bind:value={placement}>
<option value="top">Top</option>
<option value="bottom">Bottom</option>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
<Popover bind:placement>
{#snippet content()}
Placed at: {placement}
{/snippet}
Click me
</Popover>
The library automatically applies left/top from middlewareData.arrow to the arrow element, positioning it along the edge to stay centered on the reference. It also resets stale right/bottom values when placement changes.
<script>
import { createFloating, offset, flip, shift } from 'svelte-floating-attach'
let show = $state(false)
const { ref, content, arrow, arrowMiddleware } = createFloating()
</script>
<button
{@attach ref}
onmouseenter={() => show = true}
onmouseleave={() => show = false}
>
Hover me
</button>
{#if show}
<div {@attach content({
placement: 'top',
middleware: [offset(8), flip(), shift(), arrowMiddleware({ padding: 4 })],
})}>
Tooltip text
<div {@attach arrow} class="arrow"></div>
</div>
{/if}
The library positions the arrow along the correct edge (centering it on the reference) but does not push it onto the edge itself — that depends on your arrow's size and visual design. Use onComputed to offset the arrow so it pokes out of the floating element:
<script>
import { createFloating, offset, flip, shift } from 'svelte-floating-attach'
let show = $state(false)
let arrowEl = $state()
const { ref, content, arrow, arrowMiddleware } = createFloating()
function onComputed(data) {
if (!arrowEl) return
// The side of the floating element that faces the reference:
// placement "top" → arrow sits on "bottom" edge
// placement "bottom" → arrow sits on "top" edge
// placement "left" → arrow sits on "right" edge
// placement "right" → arrow sits on "left" edge
const side = data.placement.split('-')[0]
const oppositeSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[side]
// Nudge the arrow outward so it straddles the edge (half in, half out)
arrowEl.style[oppositeSide] = `${-arrowEl.offsetWidth / 2}px`
}
</script>
<button
{@attach ref}
onmouseenter={() => show = true}
onmouseleave={() => show = false}
>
Hover me
</button>
{#if show}
<div {@attach content({
placement: 'top',
middleware: [offset(8), flip(), shift(), arrowMiddleware({ padding: 4 })],
onComputed,
})}>
Tooltip text
<div bind:this={arrowEl} {@attach arrow} class="arrow"></div>
</div>
{/if}
<style>
.arrow {
position: absolute;
width: 10px;
height: 10px;
background: inherit;
transform: rotate(45deg);
}
</style>
Without this offset the arrow stays fully inside the floating element. The offset value controls how much it pokes out — use -offsetWidth / 2 to center it on the edge, or any other value that fits your design.
<script>
import { createFloating, createVirtualElement, offset } from 'svelte-floating-attach'
const virtual = createVirtualElement({
getBoundingClientRect: { x: 0, y: 0, top: 0, left: 0, bottom: 0, right: 0, width: 0, height: 0 }
})
const { setVirtualReference, content } = createFloating()
setVirtualReference(virtual)
let show = $state(false)
</script>
<div
onmouseenter={() => show = true}
onmouseleave={() => show = false}
onmousemove={(e) => {
virtual.update({
getBoundingClientRect: {
x: e.clientX, y: e.clientY,
top: e.clientY, left: e.clientX,
bottom: e.clientY, right: e.clientX,
width: 0, height: 0,
}
})
}}
>
Hover area
</div>
{#if show}
<div {@attach content({ strategy: 'fixed', placement: 'right-start', middleware: [offset(16)] })}>
Following cursor
</div>
{/if}
createFloating()Creates a floating instance. Returns:
| Property | Type | Description |
|---|---|---|
ref |
Attachment |
Attach to the reference/trigger element |
content |
(options?) => Attachment |
Returns an attachment for the floating element |
arrow |
Attachment |
Attach to the arrow/caret element. left/top styles are applied automatically from middlewareData.arrow after each position computation. |
arrowMiddleware |
(options?) => Middleware |
Creates arrow middleware using the captured arrow element. Use in the middleware array passed to content(). |
setVirtualReference |
(el: VirtualElement) => void |
Set a virtual element as the reference |
FloatingContentOptionsOptions passed to content(). See Floating UI docs for details on placement, strategy, and middleware.
| Option | Type | Default | Description |
|---|---|---|---|
placement |
Placement |
'bottom' |
Where to place the floating element |
strategy |
Strategy |
'absolute' |
CSS positioning strategy |
middleware |
Middleware[] |
undefined |
Floating UI middleware array |
autoUpdate |
boolean | AutoUpdateOptions |
true |
Auto-update on scroll/resize |
onComputed |
(data: ComputePositionReturn) => void |
undefined |
Callback after position computation |
createVirtualElement(config)Creates a mutable virtual element for non-DOM references (e.g., cursor position). Call .update(config) to change the position.
All middleware and types from @floating-ui/dom are re-exported for convenience, so you only need one import source.
MIT