<script lang="ts">
import BellIcon from "phosphor-svelte/lib/BellIcon";
import { Button } from "kumo-svelte/components/button";
import * as Popover from "kumo-svelte/components/popover";
</script>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props} shape="square" icon={BellIcon} aria-label="Notifications" />
{/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>
Import
import * as Popover from "kumo-svelte/components/popover"; Usage
<script lang="ts">
import { Button } from "kumo-svelte/components/button";
import * as Popover from "kumo-svelte/components/popover";
</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 with controlled state, they serve a different purpose than tooltips.
| 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 by default, or custom controlled triggers. |
| Keyboard | Not focusable. | Focus moves inside 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.
Examples
Basic Popover
<script lang="ts">
import { Button } from "kumo-svelte/components/button";
import * as Popover from "kumo-svelte/components/popover";
</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 } from "kumo-svelte/components/button";
import * as Popover from "kumo-svelte/components/popover";
</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 {...props} variant="secondary" size="sm">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 } from "kumo-svelte/components/button";
import * as Popover from "kumo-svelte/components/popover";
const sides = ["bottom", "top", "left", "right"] as const;
</script>
<div class="flex flex-wrap gap-4">
{#each sides as side}
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button {...props} variant="secondary">{side[0].toUpperCase() + side.slice(1)}</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content {side}>
<Popover.Title>{side[0].toUpperCase() + side.slice(1)}</Popover.Title>
<Popover.Description>Popover on {side}.</Popover.Description>
</Popover.Content>
</Popover.Root>
{/each}
</div>
Custom Content
Popovers can contain any content, including custom layouts with avatars, buttons, and more.
<script lang="ts">
import { Button } from "kumo-svelte/components/button";
import * as Popover from "kumo-svelte/components/popover";
</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-hairline pt-3">
<Button variant="secondary" size="sm" class="flex-1">Profile</Button>
<Popover.Close>
{#snippet child({ props })}
<Button {...props} variant="ghost" size="sm" class="flex-1">Sign Out</Button>
{/snippet}
</Popover.Close>
</div>
</Popover.Content>
</Popover.Root>
Open on Hover
Use controlled state to open the popover on hover. This keeps hover behavior explicit in Svelte.
<script lang="ts">
import { Button } from "kumo-svelte/components/button";
import * as Popover from "kumo-svelte/components/popover";
let open = $state(false);
let timer: ReturnType<typeof setTimeout> | undefined;
function show() {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
open = true;
}, 200);
}
function hide() {
if (timer) clearTimeout(timer);
open = false;
}
</script>
<Popover.Root bind:open>
<Popover.Trigger onmouseenter={show} onmouseleave={hide}>
{#snippet child({ props })}
<Button {...props} variant="secondary">Hover Me</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content onmouseenter={show} onmouseleave={hide}>
<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 {...props} variant="secondary" size="sm">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 element with getBoundingClientRect().
| Name | Status | |
|---|---|---|
| api-gateway | Active | |
| auth-service | Active | |
| worker-prod | Paused |
<script lang="ts">
import DotsThreeIcon from "phosphor-svelte/lib/DotsThreeIcon";
import { Button } from "kumo-svelte/components/button";
import * as Popover from "kumo-svelte/components/popover";
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 | undefined>();
let anchor = $state<{ getBoundingClientRect: () => DOMRect } | undefined>();
let open = $derived(Boolean(selectedRow));
function handleEdit(event: MouseEvent, id: string) {
const row = (event.currentTarget as HTMLElement).closest("tr");
anchor = {
getBoundingClientRect: () => (row ?? (event.currentTarget as HTMLElement)).getBoundingClientRect(),
};
selectedRow = id;
}
</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={DotsThreeIcon}
aria-label={`Actions for ${row.name}`}
onclick={(event) => handleEdit(event, row.id)}
/>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<Popover.Root {open} onOpenChange={(nextOpen) => !nextOpen && (selectedRow = undefined)}>
<Popover.Content side="left" {anchor}>
<Popover.Title>Edit {rows.find((row) => row.id === selectedRow)?.name}</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 {...props} size="sm" variant="secondary">Close</Button>
{/snippet}
</Popover.Close>
</div>
</Popover.Content>
</Popover.Root>
</div>
API Reference
Popover
| Prop | Type | Default |
|---|---|---|
| children | Snippet | — |
| open | boolean | false |
| onOpenChange | (open: boolean) => void | — |
| onOpenChangeComplete | (open: boolean) => void | — |
Popover.Close
No component-specific props. This component accepts child content or standard forwarded attributes.
Popover.Content
| Prop | Type | Default |
|---|---|---|
| align | KumoPopoverAlign | "center" |
| anchor | PopoverPrimitive.ContentProps["customAnchor"] | — |
| container | PortalProps["to"] | — |
| positionMethod | "absolute" | "fixed" | "absolute" |
| side | KumoPopoverSide | KUMO_POPOVER_DEFAULT_VARIANTS.side |
Popover.Description
| Prop | Type | Default |
|---|---|---|
| children | Snippet | — |
Popover.Header
| Prop | Type | Default |
|---|---|---|
| children | Snippet | — |
Popover.Portal
| Prop | Type | Default |
|---|---|---|
| children | Snippet | — |
| to | PortalProps["to"] | — |
Popover.Title
| Prop | Type | Default |
|---|---|---|
| children | Snippet | — |
Popover.Trigger
No component-specific props. This component accepts child content or standard forwarded attributes.