Popover
Floating content anchored to a trigger.
Can I tell you a secret?
you're awesome
Props
Features
Section titled “Features”- 🌳 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>
<script lang="ts"> import { Popover } from "melt/components";</script>
<Popover> {#snippet children(popover)} <button {...popover.trigger}> Click me to open the Popover </button> <div {...popover.content}>Content</div> {/snippet}</Popover>
Multiple Triggers (Singleton Pattern)
Section titled “Multiple Triggers (Singleton Pattern)”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>
<script lang="ts"> import { Popover } from "melt/components"; import { mergeAttrs } from "$lib/utils/attribute";
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>
<Popover> {#snippet children(popover)} {#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> {/snippet}</Popover>
Customizing floating elements
Section titled “Customizing floating elements”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.
API Reference
Section titled “API Reference”Constructor Props
The props that are passed when calling
new Popover()
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>; };};
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()
new Popover()
-
ids
{ popover: string } & { trigger: string; content: string }{ popover: string } & { trigger: string; content: string } -
trigger
{readonly onfocus: (event: FocusEvent) => voidreadonly 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: stringreadonly onclick: (e: Event) => void} & { "data-melt-popover-trigger": string }{readonly onfocus: (event: FocusEvent) => voidreadonly 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: stringreadonly onclick: (e: Event) => void} & { "data-melt-popover-trigger": string }The trigger that toggles the value. -
content
{readonly onfocus: (event: FocusEvent) => voidreadonly 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: stringreadonly popover: "manual"readonly ontoggle: (e: ToggleEvent & { currentTarget: EventTarget & HTMLElement },) => voidreadonly tabindex: -1readonly inert: booleanreadonly "data-open": "" | undefined} & { "data-melt-popover-content": string }{readonly onfocus: (event: FocusEvent) => voidreadonly 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: stringreadonly popover: "manual"readonly ontoggle: (e: ToggleEvent & { currentTarget: EventTarget & HTMLElement },) => voidreadonly tabindex: -1readonly inert: booleanreadonly "data-open": "" | undefined} & { "data-melt-popover-content": string }