Kumo Svelte
  • Home
  • Installation
  • Colors
  • Accessibility
  • Changelog
  • Autocomplete
  • Badge
  • Banner
  • Breadcrumbs
  • Button
  • Checkbox
  • Clipboard Text
  • Cloudflare Logo
  • CodeHighlighted
  • Collapsible
  • Combobox
  • Command Palette
  • Date Picker
  • Dialog
  • Dropdown
  • Empty
  • Flow
  • Grid
  • Input
  • InputArea
  • InputGroup
  • Label
  • Layer Card
  • Link
  • Loader
  • MenuBar
  • Meter
  • Pagination
  • Popover
  • Radio
  • Select
  • Sensitive Input
  • Sidebar
  • Skeleton Line
  • Switch
  • Table
  • Table of Contents
  • Tabs
  • Text
  • Toast
  • Toolbar
  • Tooltip
  • Charts
  • Colors
  • Timeseries
  • Maps
  • Sankey
  • Custom Chart
  • Page Header
  • Resource List
  • Delete Resource
Popover
kumo-svelte

Popover

An accessible popup anchored to a trigger element, used for displaying rich content like menus, forms, or additional information.

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

TooltipPopover
PurposeShort, non-interactive text labels for identification.Rich, interactive content containers.
ContentPlain text only.Any content: links, buttons, forms, images.
TriggerHover or focus.Click by default, or custom controlled triggers.
KeyboardNot 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().

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

PropTypeDefault
childrenSnippet—
openbooleanfalse
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

PropTypeDefault
alignKumoPopoverAlign"center"
anchorPopoverPrimitive.ContentProps["customAnchor"]—
containerPortalProps["to"]—
positionMethod"absolute" | "fixed""absolute"
sideKumoPopoverSideKUMO_POPOVER_DEFAULT_VARIANTS.side

Popover.Description

PropTypeDefault
childrenSnippet—

Popover.Header

PropTypeDefault
childrenSnippet—

Popover.Portal

PropTypeDefault
childrenSnippet—
toPortalProps["to"]—

Popover.Title

PropTypeDefault
childrenSnippet—

Popover.Trigger

No component-specific props. This component accepts child content or standard forwarded attributes.

On this page

  • Import
  • Usage
  • Popover vs Tooltip
  • Examples
    • Basic Popover
    • With Close Button
    • Positioning
    • Custom Content
    • Open on Hover
    • Virtual Anchor
  • API Reference
    • Popover
    • Popover.Close
    • Popover.Content
    • Popover.Description
    • Popover.Header
    • Popover.Portal
    • Popover.Title
    • Popover.Trigger