Popover
Rich content in a floating panel, opened by a button.
Melt UI Popover
Dimensions
<script lang="ts">
import { Input } from '#lib/components/ui/input/index.js';
import { Label } from '#lib/components/ui/label/index.js';
import * as Popover from '#lib/components/ui/popover/index.js';
</script>
<Popover.Root>
<Popover.Trigger>Open popover</Popover.Trigger>
<Popover.Content>
<div class="stack">
<strong>Dimensions</strong>
<div class="field">
<Label for="popover-width">Width</Label>
<Input id="popover-width" value="100%" />
</div>
</div>
</Popover.Content>
</Popover.Root>
<style>
.stack {
display: grid;
gap: var(--dui-space-3);
}
.field {
display: grid;
gap: var(--dui-space-2);
}
</style>
Installation
npx distill-ui add popoverIt also copies Button, which Popover uses.
It installs @floating-ui/domandmelt if you don't have them yet.
- Set up the tokens once, if you haven't: see manual installation.
- Install the packages it needs:
npm install @floating-ui/dom@^1.6.0 melt@0.44.0 - Copy Button too, which it uses.
- Copy these files into your project:
import { getContext, setContext } from 'svelte';
import type { Popover } from 'melt/builders';
type PopoverContext = { popover: Popover };
const key = Symbol('distill-popover');
export const setPopoverContext = (ctx: PopoverContext) => setContext(key, ctx);
export const getPopoverContext = () => getContext<PopoverContext>(key);
import Content from './popover-content.svelte';
import Trigger from './popover-trigger.svelte';
import Root from './popover.svelte';
export { Root, Trigger, Content };
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { getPopoverContext } from './context.js';
let {
class: className,
children,
style,
...restProps
}: HTMLAttributes<HTMLDivElement> = $props();
const { popover } = getPopoverContext();
</script>
<!-- Uses the native popover API (top layer), so no portal is needed. -->
<div
{...restProps}
{...popover.content}
role="dialog"
style="{popover.content.style}; {style ?? ''}"
class={['content', className]}
>
{@render children?.()}
</div>
<!--
Options you can set from a parent or on the popover content itself:
--dui-popover-width, --dui-popover-padding, --dui-popover-radius, --dui-popover-bg, --dui-popover-fg
-->
<style>
.content {
width: var(--dui-popover-width, 18rem);
margin: 0;
padding: var(--dui-popover-padding, var(--dui-space-4));
border: 0;
border-radius: var(--dui-popover-radius, var(--dui-radius-md));
background: var(--dui-popover-bg, var(--dui-color-popover));
color: var(--dui-popover-fg, var(--dui-color-popover-foreground));
box-shadow:
0 0 0 1px color-mix(in oklch, var(--dui-color-foreground) 10%, transparent),
var(--dui-shadow-md);
font-size: var(--dui-text-sm);
line-height: var(--dui-text-sm-line-height);
outline: none;
transition:
opacity var(--dui-duration-fast) var(--dui-ease-out),
scale var(--dui-duration-fast) var(--dui-ease-out);
@starting-style {
opacity: 0;
scale: 0.95;
}
}
</style>
<script lang="ts">
import type { HTMLButtonAttributes } from 'svelte/elements';
import { Button, type ButtonSize, type ButtonVariant } from '../button/index.js';
import { getPopoverContext } from './context.js';
let {
variant = 'outline',
children,
...restProps
}: HTMLButtonAttributes & { variant?: ButtonVariant; size?: ButtonSize } = $props();
const { popover } = getPopoverContext();
</script>
<Button
{...restProps}
{...popover.trigger}
aria-haspopup="dialog"
aria-expanded={popover.open}
aria-controls={popover.ids.content}
{variant}
>
{@render children?.()}
</Button>
<script lang="ts">
import type { Placement } from '@floating-ui/dom';
import { Popover } from 'melt/builders';
import type { Snippet } from 'svelte';
import { setPopoverContext } from './context.js';
let {
open = $bindable(false),
placement = 'bottom',
children
}: { open?: boolean; placement?: Placement; children?: Snippet } = $props();
const popover = new Popover({
open: () => open,
onOpenChange: (v) => (open = v),
floatingConfig: () => ({ computePosition: { placement }, offset: { mainAxis: 4 } })
});
setPopoverContext({ popover });
</script>
{@render children?.()}
Customizing
Set any of these on the component or on a parent element. See Customizing for how they work.
| Custom property | Default |
|---|---|
--dui-popover-width | 18rem |
--dui-popover-padding | var(--dui-space-4) |
--dui-popover-radius | var(--dui-radius-md) |
--dui-popover-bg | var(--dui-color-popover) |
--dui-popover-fg | var(--dui-color-popover-foreground) |