Skip to content

Popover

Floating content anchored to a trigger.

Can I tell you a secret?

you're awesome

Props


  • 🌳 Uses native popover attribute, no need for portalling
  • 🧠 Smart focus management, auto-closes when tabbed out
  • 🎈 Fully customize Floating UI’s options
  • 🪺 Nested popovers
  • 🎨 Animation support
<script lang="ts">
import { Popover } from "melt/builders";
const popover = new Popover();
</script>
<button {...popover.trigger}>
Click me to open the Popover
</button>
<div {...popover.content}>
<div {...popover.arrow}></div>
Content
</div>

A single popover can be controlled by multiple triggers, creating a singleton pattern where one popover instance serves different contextual content. The popover will position itself relative to whichever trigger was last clicked, and you can dynamically change the content based on the active trigger.

Click any button to see contextual content in the shared popover.

Click a trigger to see contextual content

<script lang="ts">
import { Popover } from "melt/builders";
import { mergeAttrs } from "$lib/utils/attribute";
const popover = new Popover();
let activeTrigger = $state<string | null>(null);
const triggerData = {
user: { label: "User Profile", content: "User settings and preferences" },
notifications: { label: "Notifications", content: "3 unread notifications" },
help: { label: "Help", content: "FAQ and support options" }
};
</script>
{#each Object.entries(triggerData) as [key, data]}
<button {...mergeAttrs(popover.trigger, {
onclick: () => activeTrigger = key
})}>
{data.label}
</button>
{/each}
<div {...popover.content}>
{#if activeTrigger && triggerData[activeTrigger]}
<h3>{triggerData[activeTrigger].label}</h3>
<p>{triggerData[activeTrigger].content}</p>
{:else}
<p>Click a trigger to see contextual content</p>
{/if}
</div>

Floating elements use Floating UI under the hood. To this end, we expose a floatingConfig option, which can be used to control the underlying computePosition function, its middlewares, and the resulting styling that is applied.

Constructor Props

The props that are passed when calling
new Popover()
    export type PopoverProps = {
    /**
    * If the Popover is open.
    *
    * When passing a getter, it will be used as source of truth,
    * meaning that the value only changes when the getter returns a new value.
    *
    * Otherwise, if passing a static value, it'll serve as the default value.
    *
    *
    * @default false
    */
    open?: MaybeGetter<boolean | undefined>;
    /**
    * Called when the value is supposed to change.
    */
    onOpenChange?: (value: boolean) => void;
    /**
    * If the popover visibility should be controlled by the user.
    *
    * @default false
    */
    forceVisible?: MaybeGetter<boolean | undefined>;
    /**
    * Config to be passed to `useFloating`
    */
    floatingConfig?: UseFloatingArgs["config"];
    /**
    * If the popover should have the same width as the trigger
    *
    * @default false
    */
    sameWidth?: MaybeGetter<boolean | undefined>;
    /**
    * If the popover should close when clicking escape.
    *
    * @default true
    */
    closeOnEscape?: MaybeGetter<boolean | undefined>;
    /**
    * If the popover should close when clicking outside.
    * Alternatively, accepts a function that receives the clicked element,
    * and returns if the popover should close.
    *
    * @default true
    */
    closeOnOutsideClick?: CloseOnOutsideClickProp;
    focus?: {
    /**
    * Which element to focus when the popover opens.
    * Can be a selector string, an element, or a Getter for those.
    * If null, the focus remains on the trigger element.
    *
    * Defaults to the popover content element.
    */
    onOpen?: MaybeGetter<HTMLElement | string | null | undefined>;
    /**
    * Which element to focus when the popover closes.
    * Can be a selector string, an element, or a Getter for those.
    * If null, the focus goes to the document body.
    *
    * Defaults to the last used trigger element.
    */
    onClose?: MaybeGetter<HTMLElement | string | null | undefined>;
    /**
    * If focus should be trapped inside the popover content when open.
    *
    * @default false
    */
    trap?: MaybeGetter<boolean | undefined>;
    };
    };

Properties

The properties returned from
new Popover()
  • ids

    { popover: string } & { trigger: string; content: string }
  • trigger

    {
    readonly onfocus: (event: FocusEvent) => void
    readonly onfocusout: (event: FocusEvent) => Promise<void>
    readonly style: `--melt-invoker-width: ${string}; --melt-invoker-height: ${string}; --melt-invoker-x: ${string}; --melt-invoker-y: ${string}; --melt-popover-available-width: ${string}; --melt-popover-available-height: ${string}`
    readonly popovertarget: string
    readonly onclick: (e: Event) => void
    } & { "data-melt-popover-trigger": string }
    The trigger that toggles the value.
  • content

    {
    readonly onfocus: (event: FocusEvent) => void
    readonly onfocusout: (event: FocusEvent) => Promise<void>
    readonly style: `--melt-invoker-width: ${string}; --melt-invoker-height: ${string}; --melt-invoker-x: ${string}; --melt-invoker-y: ${string}; --melt-popover-available-width: ${string}; --melt-popover-available-height: ${string}`
    readonly id: string
    readonly popover: "manual"
    readonly ontoggle: (
    e: ToggleEvent & { currentTarget: EventTarget & HTMLElement },
    ) => void
    readonly tabindex: -1
    readonly inert: boolean
    readonly "data-open": "" | undefined
    } & { "data-melt-popover-content": string }