<script lang="ts">
import { Bell } from 'phosphor-svelte';
import { Button, Popover } from '$lib';
</script>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button shape="square" icon={Bell} aria-label="Notifications" {...props} />
{/snippet}
</Popover.Trigger>
<Popover.Content>
<Popover.Title>Notifications</Popover.Title>
<Popover.Description>You are all caught up. Good job!</Popover.Description>
</Popover.Content>
</Popover.Root> Installation
Barrel
import { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverRoot, PopoverTitle, PopoverTrigger } from 'kumo-svelte'; Granular
import { Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverRoot, PopoverTitle, PopoverTrigger } from 'kumo-svelte/components/popover';Usage
<script> import { Button, Popover } from "kumo-svelte";</script><Popover.Root> <Popover.Trigger> {#snippet child({ props })} <Button {...props}>Open</Button> {/snippet} </Popover.Trigger> <Popover.Content> <Popover.Title>Popover Title</Popover.Title> <Popover.Description>Popover content goes here.</Popover.Description> </Popover.Content></Popover.Root>Popover vs Tooltip
While popovers can be triggered on hover (using openOnHover), they serve a
different purpose than tooltips. Understanding when to use each is important
for accessibility and user experience.
| Tooltip | Popover | |
|---|---|---|
| Purpose | Short, non-interactive text labels for identification | Rich, interactive content containers |
| Content | Plain text only | Any content: links, buttons, forms, images |
| Trigger | Hover or focus | Click (default) or hover |
| ARIA Role | role="tooltip" | aria-haspopup |
| Keyboard | Not focusable | Focus moves inside, traps when open |
Use a Tooltip when you need to label an icon button or provide a brief explanation. Use a Popover when users need to interact with the content inside, such as clicking links, filling out forms, or dismissing with a button.
Examples
Basic Popover
<script lang="ts">
import { Button, Popover } from '$lib';
</script>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props}>Open Popover</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content>
<Popover.Title>Popover Title</Popover.Title>
<Popover.Description>
This is a basic popover with a title and description.
</Popover.Description>
</Popover.Content>
</Popover.Root> With Close Button
<script lang="ts">
import { Button, Popover } from '$lib';
</script>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props}>Open Settings</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content>
<Popover.Title>Settings</Popover.Title>
<Popover.Description>Configure your preferences below.</Popover.Description>
<div class="mt-3">
<Popover.Close>
{#snippet child({ props })}
<Button variant="secondary" size="sm" {...props}>Close</Button>
{/snippet}
</Popover.Close>
</div>
</Popover.Content>
</Popover.Root> Positioning
Use the side prop to control where the popover appears relative to the
trigger.
<script lang="ts">
import { Button, Popover } from '$lib';
</script>
<div class="flex flex-wrap gap-4">
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button variant="secondary" {...props}>Bottom</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content side="bottom">
<Popover.Title>Bottom</Popover.Title>
<Popover.Description>Popover on bottom (default).</Popover.Description>
</Popover.Content>
</Popover.Root>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button variant="secondary" {...props}>Top</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content side="top">
<Popover.Title>Top</Popover.Title>
<Popover.Description>Popover on top.</Popover.Description>
</Popover.Content>
</Popover.Root>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button variant="secondary" {...props}>Left</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content side="left">
<Popover.Title>Left</Popover.Title>
<Popover.Description>Popover on left.</Popover.Description>
</Popover.Content>
</Popover.Root>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button variant="secondary" {...props}>Right</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content side="right">
<Popover.Title>Right</Popover.Title>
<Popover.Description>Popover on right.</Popover.Description>
</Popover.Content>
</Popover.Root>
</div> Custom Content
Popovers can contain any content, including custom layouts with avatars, buttons, and more.
<script lang="ts">
import { Button, Popover } from '$lib';
</script>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props}>User Profile</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-64">
<div class="flex items-center gap-3">
<div class="size-10 rounded-full bg-kumo-recessed"></div>
<div>
<Popover.Title>Jane Doe</Popover.Title>
<p class="text-sm text-kumo-subtle">jane@example.com</p>
</div>
</div>
<div class="mt-3 flex gap-2 border-t border-kumo-line pt-3">
<Button variant="secondary" size="sm" class="flex-1">Profile</Button>
<Popover.Close>
{#snippet child({ props })}
<Button variant="ghost" size="sm" class="flex-1" {...props}>Sign Out</Button>
{/snippet}
</Popover.Close>
</div>
</Popover.Content>
</Popover.Root> Open on Hover
Use openOnHover on the trigger to open the popover when the user hovers over
it. You can also specify a delay in milliseconds before the popover appears.
<script lang="ts">
import { Button, Popover } from '$lib';
</script>
<Popover.Root>
<Popover.Trigger openOnHover openDelay={200}>
{#snippet child({ props })}
<Button variant="secondary" {...props}>Hover Me</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content>
<Popover.Title>Hover Triggered</Popover.Title>
<Popover.Description>
This popover opens on hover with a 200ms delay. It can still contain interactive content like
buttons and links.
</Popover.Description>
<div class="mt-3">
<Popover.Close>
{#snippet child({ props })}
<Button variant="secondary" size="sm" {...props}>Got it</Button>
{/snippet}
</Popover.Close>
</div>
</Popover.Content>
</Popover.Root> Virtual Anchor
Use the anchor prop on Popover.Content to position the popover against an
element other than the trigger, or against a virtual point (e.g., a DOMRect from getBoundingClientRect()). This is useful when the trigger and the
desired anchor are in different component trees.
| Name | Status | |
|---|---|---|
| api-gateway | Active | |
| auth-service | Active | |
| worker-prod | Paused |
<script lang="ts">
import { DotsThree } from 'phosphor-svelte';
import { Button, Popover } from '$lib';
const rows = [
{ id: '1', name: 'api-gateway', status: 'Active' },
{ id: '2', name: 'auth-service', status: 'Active' },
{ id: '3', name: 'worker-prod', status: 'Paused' }
];
let selectedRow = $state<string | null>(null);
let anchorRect = $state<DOMRect | null>(null);
let open = $state(false);
const selectedName = $derived(rows.find((row) => row.id === selectedRow)?.name);
const anchor = $derived.by(() => {
if (!anchorRect) return null;
const rect = anchorRect;
return { getBoundingClientRect: () => rect };
});
function handleEdit(id: string, event: MouseEvent) {
const row = (event.currentTarget as HTMLElement).closest('tr');
if (!row) return;
anchorRect = (event.currentTarget as HTMLElement).getBoundingClientRect();
selectedRow = id;
open = true;
}
$effect(() => {
if (!open) {
selectedRow = null;
anchorRect = null;
}
});
</script>
<div class="w-full">
<div class="overflow-hidden rounded-lg border border-kumo-hairline">
<table class="w-full text-sm">
<thead class="bg-kumo-elevated">
<tr>
<th class="px-4 py-2 text-left font-medium">Name</th>
<th class="px-4 py-2 text-left font-medium">Status</th>
<th class="w-12 px-4 py-2"></th>
</tr>
</thead>
<tbody class="divide-y divide-kumo-hairline">
{#each rows as row (row.id)}
<tr
class={selectedRow === row.id ? 'bg-kumo-recessed' : 'bg-kumo-base'}
>
<td class="px-4 py-2 font-mono">{row.name}</td>
<td class="px-4 py-2 text-kumo-subtle">{row.status}</td>
<td class="px-4 py-2">
<Button
size="xs"
variant="ghost"
shape="square"
icon={DotsThree}
aria-label={`Actions for ${row.name}`}
onclick={(event: MouseEvent) => handleEdit(row.id, event)}
/>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<Popover.Root bind:open>
<Popover.Content side="left" {anchor}>
<Popover.Title>Edit {selectedName}</Popover.Title>
<Popover.Description>
The popover anchors to the selected row, not the icon button.
</Popover.Description>
<div class="mt-3">
<Popover.Close>
{#snippet child({ props })}
<Button size="sm" variant="secondary" {...props}>Close</Button>
{/snippet}
</Popover.Close>
</div>
</Popover.Content>
</Popover.Root>
</div> API Reference
Popover
The root component that manages the popover's open state.
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | - | Controlled open state. |
| defaultOpen | boolean | false | Initial open state for uncontrolled usage. |
| onOpenChange | (open: boolean) => void | - | Called when the open state changes. |
| modal | boolean | false | Whether the popover is modal. |
Popover.Trigger
A button that opens the popover when clicked. Use a render prop to render
your own element.
| Prop | Type | Default | Description |
|---|---|---|---|
| child | Snippet<[{ props: Record<string, unknown> }]> | - | Custom trigger render target. |
Popover.Content
The container for popover content. Controls positioning via side, align, sideOffset, and alignOffset props. Use the anchor prop to position
against a custom element or virtual point instead of the trigger. Use positionMethod="fixed" when the popover needs to escape stacking contexts,
such as when inside sticky headers.
| Prop | Type | Default | Description |
|---|---|---|---|
| side | 'top' | 'right' | 'bottom' | 'left' | "bottom" | Preferred side of the trigger. |
| anchor | HTMLElement | { getBoundingClientRect: () => DOMRect } | null | - | Element or virtual element to position the popover against. |
| align | 'start' | 'center' | 'end' | "center" | Alignment relative to the anchor. |
| sideOffset | number | 0 | Distance from the anchor side. |
| alignOffset | number | 0 | Offset along the alignment axis. |
| positionMethod | 'absolute' | 'fixed' | "absolute" | CSS positioning strategy. |
| container | HTMLElement | string | document.body | Portal container for custom roots or Shadow DOM. |
Popover.Title
A heading that labels the popover for accessibility.
| Prop | Type | Default | Description |
|---|---|---|---|
| No component-specific props. Accepts standard HTML attributes. | |||
Popover.Description
A paragraph providing additional context about the popover content.
| Prop | Type | Default | Description |
|---|---|---|---|
| No component-specific props. Accepts standard HTML attributes. | |||
Popover.Close
A button that closes the popover when clicked. Use a render prop to render
your own element.
| Prop | Type | Default | Description |
|---|---|---|---|
| child | Snippet<[{ props: Record<string, unknown> }]> | - | Custom close render target. |