Popover
kumo-svelte
<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.

TooltipPopover
PurposeShort, non-interactive text labels for identificationRich, interactive content containers
ContentPlain text onlyAny content: links, buttons, forms, images
TriggerHover or focusClick (default) or hover
ARIA Rolerole="tooltip"aria-haspopup
KeyboardNot focusableFocus 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.

NameStatus
api-gatewayActive
auth-serviceActive
worker-prodPaused
<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.

PropTypeDefaultDescription
open boolean-Controlled open state.
defaultOpen booleanfalseInitial open state for uncontrolled usage.
onOpenChange (open: boolean) => void-Called when the open state changes.
modal booleanfalseWhether the popover is modal.

Popover.Trigger

A button that opens the popover when clicked. Use a render prop to render your own element.

PropTypeDefaultDescription
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.

PropTypeDefaultDescription
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 number0Distance from the anchor side.
alignOffset number0Offset along the alignment axis.
positionMethod 'absolute' | 'fixed'"absolute"CSS positioning strategy.
container HTMLElement | stringdocument.bodyPortal container for custom roots or Shadow DOM.

Popover.Title

A heading that labels the popover for accessibility.

PropTypeDefaultDescription
No component-specific props. Accepts standard HTML attributes.

Popover.Description

A paragraph providing additional context about the popover content.

PropTypeDefaultDescription
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.

PropTypeDefaultDescription
child Snippet<[{ props: Record<string, unknown> }]>-Custom close render target.