Please note that this is a Svelte-only guide based around the attachments feature introduced in Svelte v5.29
.
Summary
The following will guide you through integrating Floating UI in Svelte and generating a baseline attachment that can be used to scaffold any number of custom popover interfaces, including but not limited to: popovers, tooltips, dialogs, drawers, combobox, context menus, and more.
Accessibility Warning
This guide is not a drop-in replacement for Skeleton’s Svelte Popovers as it does not replicate all recommended accessbility features out of the box (such as aria, focus states, keyboard interactions, etc). These features are out of scope of this guide and will be your responsibility to handle before moving to a production environment.
Target Audience
This guide is intended for advanced users that wish to integrate directly with Floating UI, build completely custom floating interfaces, and go beyond the scope of Skeleton’s Svelte Popovers. This can be used to generate interfaces not covered by Skeleton, such as GDPR popovers and context menus.
Installing Floating UI
To begin, install the standard version of Floating UI.
npm install @floating-ui/dom
If this is your first time using Floating UI, we recommend following their guided tutorial to learn the basics.
Creating a Svelte Attachment
Next, let’s generate our custom attachment. If you’re working with SvelteKit, we recommend adding this to /src/lib/attachments/floating.svelte.ts
.
import { createAttachmentKey } from 'svelte/attachments';import { computePosition, autoUpdate, flip, offset, type Placement } from '@floating-ui/dom';
interface PopoverOptions { interaction?: 'click' | 'hover'; placement?: Placement;}
export class Popover { private options: PopoverOptions = { interaction: 'click', placement: 'bottom-start' }; private open = $state(false); private referenceElement: HTMLElement | undefined = $state(); private floatingElement: HTMLElement | undefined = $state();
constructor(options?: PopoverOptions) { if (options) this.options = { ...this.options, ...options }; $effect(() => { if (!this.referenceElement || !this.floatingElement) return; return autoUpdate(this.referenceElement, this.floatingElement, this.#updatePosition); }); }
reference() { const attrs = { [createAttachmentKey()]: (node: HTMLElement) => { this.referenceElement = node; }, onclick: () => {}, onmouseover: () => {}, onmouseout: () => {} }; // If click interaction if (this.options.interaction === 'click') { attrs['onclick'] = () => { this.open = !this.open; }; } // If hover interaction if (this.options.interaction === 'hover') { attrs['onclick'] = () => { this.open = !this.open; }; attrs['onmouseover'] = () => { this.open = true; }; attrs['onmouseout'] = () => { this.open = false; }; } return attrs; }
floating() { return { [createAttachmentKey()]: (node: HTMLElement) => { this.floatingElement = node; } }; }
isOpen() { return this.open; }
#updatePosition = async () => { if (!this.referenceElement || !this.floatingElement) { return; } const position = await computePosition(this.referenceElement, this.floatingElement, { placement: this.options.placement, middleware: [flip(), offset(8)] }); const { x, y } = position; Object.assign(this.floatingElement.style, { left: `${x}px`, top: `${y}px` }); };}
This attachment will handle a number of tasks:
- This imports the Svelte attachment and Floating UI dependencies.
- Scaffolds a simple
PopoverOptions
interface, which defines our configuraton options. - Implement the
Popover
class, which handles all the business logic for creating and using the attachment. - And of course sets the default configuration via
options
.
We’ll cover each additional method below.
reference()
When implemented, this is spread to the Trigger element and handles interaction such as click
and hover
.
floating()
When implemented, this is spread to the Popover element itself. This uses createAttachmentKey to generate the attachment relationship itself.
isOpen()
Returns the current open
state as a boolean value. We’ll use this to show and hide the popover.
#updatePosition()
This scaffolds computePosition, which handles most of Floating UI’s functionality and configuration.
Making the Tooltip Float
Floating UI requires these CSS styles to ensure the popover element “floats” over other UI. For this guide we’ll handle this with a convention by adding the following your to global stylesheet. For SvelteKit that’s located in /src/app.css
.
[data-floating] { width: max-content; position: absolute; top: 0; left: 0;}
Usage
Popover
Add the following to any page within your application to generate a basic popover.
<script lang="ts"> import { slide } from 'svelte/transition'; import { Popover } from '$lib/attachments/floating.svelte.js';
const popover = new Popover();</script>
<span> <button {...popover.reference()} class="btn preset-filled">Trigger</button> {#if popover.isOpen()} <div {...popover.floating()} data-floating class="card preset-filled-surface-100-900 z-10 p-4" transition:slide={{ duration: 150 }} > <p>This is an example popover.</p> </div> {/if}</span>
- First, import the Popover attachmen and generate an instance using
new Popover()
. - Next, create a wrapping
<span>
to ensure your popover is not affected by the flow of the document. - Add your trigger button and spread the
popover.reference()
- Add your popover element and spread the
popover.floating()
- Apply
data-floating
to the popover element. - Wrap the popover element with
#if popover.isOpen()
to show/hide the popover.
TIP: you can optionally import a Svelte transition, such as
slide
. Then use this to trigger animations on the open/close state for the popover, as it shows and hides.
Tooltip
Add the following to any page within your application to generate a basic tooltip.
<script lang="ts"> import { fade } from 'svelte/transition'; import { Popover } from '$lib/attachments/floating.svelte.js';
const tooltip = new Popover({ interaction: 'hover', placement: 'top' });</script>
<span> <p>This triggers a <span class="underline" {...tooltip.reference()}>tooltip</span>.</p> {#if tooltip.isOpen()} <div {...tooltip.floating()} data-floating class="card preset-filled-surface-100-900 z-10 p-4" transition:fade={{ duration: 150 }} > <p>This is an example tooltip.</p> </div> {/if}</span>
- Similar to the Popover above, we import, initialize, and scaffold the common attachment requirements.
- Unlike the Popover though, we configure
new Popover(...)
to adjust theinteraction
andplacement
settings. - We can also use a different transition, such as
fade
in this example.
Handling Accessibility
We would advise following recommended Aria APG patterns when generating popover interfaces for production use. We’ve linked a few of the common patterns below to help you get started. This covers aria
and role
attributes, keyboard interactions, and general best practices.