ShardsUI is in beta. APIs may change before 1.0.

Popover

A floating anchored panel.

<script lang="ts">
  import { Popover } from '@shardsui/svelte/popover'
</script>

<Popover.Root>
  <Popover.Trigger
    class="flex size-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100"
    aria-label="Activity"
  >
    {@render fireIcon()}
  </Popover.Trigger>
  <Popover.Portal>
    <Popover.Positioner sideOffset={8}>
      <Popover.Popup
        class="flex max-w-72 origin-(--transform-origin) flex-col gap-1 rounded-lg bg-gray-50 p-3 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
      >
        <Popover.Arrow
          class="data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
        >
          {@render arrow()}
        </Popover.Arrow>
        <Popover.Title class="text-sm font-semibold">Activity</Popover.Title>
        <Popover.Description class="text-sm text-gray-600">
          12 days active this month.
        </Popover.Description>
      </Popover.Popup>
    </Popover.Positioner>
  </Popover.Portal>
</Popover.Root>

{#snippet fireIcon()}
  <svg viewBox="0 0 24 24" fill="none" class="size-4" aria-hidden="true">
    <path
      d="M17.8186 7.96008C18.9803 9.06824 19.4816 10.4992 19.6695 11.9928C20.8623 21.3865 8.45545 24.7553 4.84302 16.7772C3.33975 13.447 4.88844 10.2793 7.31572 7.94562C8.99792 6.33156 10.6416 5.03914 11.3646 2.5C14.5716 4.58333 15.0895 8.75 15.0895 10.8333C15.0895 10.8333 17.2824 8.71744 17.8186 7.96008Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
    <path
      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
    />
  </svg>
{/snippet}

Anatomy

<script>
  import { Popover } from '@shardsui/svelte/popover'
</script>

<Popover.Root>
  <Popover.Trigger />
  <Popover.Portal>
    <Popover.Backdrop />
    <Popover.Positioner>
      <Popover.Popup>
        <Popover.Arrow />
        <Popover.Viewport>
          <Popover.Title />
          <Popover.Description />
          <Popover.Close />
        </Popover.Viewport>
      </Popover.Popup>
    </Popover.Positioner>
  </Popover.Portal>
</Popover.Root>

Examples

Opening on hover

Set openOnHover on the trigger to open the popover on hover as well as on click. Tune the timing with delay (how long the pointer must rest before it opens) and closeDelay (how long it lingers after the pointer leaves), both in milliseconds.

<script lang="ts">
  import { Popover } from '@shardsui/svelte/popover'
</script>

<Popover.Root>
  <Popover.Trigger
    openOnHover
    class="flex size-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100"
    aria-label="Activity"
  >
    {@render fireIcon()}
  </Popover.Trigger>
  <Popover.Portal>
    <Popover.Positioner sideOffset={8}>
      <Popover.Popup
        class="flex max-w-72 origin-(--transform-origin) flex-col gap-1 rounded-lg bg-gray-50 p-3 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
      >
        <Popover.Arrow
          class="data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
        >
          {@render arrow()}
        </Popover.Arrow>
        <Popover.Title class="text-sm font-semibold">Activity</Popover.Title>
        <Popover.Description class="text-sm text-gray-600">
          12 days active this month.
        </Popover.Description>
      </Popover.Popup>
    </Popover.Positioner>
  </Popover.Portal>
</Popover.Root>

{#snippet fireIcon()}
  <svg viewBox="0 0 24 24" fill="none" class="size-4" aria-hidden="true">
    <path
      d="M17.8186 7.96008C18.9803 9.06824 19.4816 10.4992 19.6695 11.9928C20.8623 21.3865 8.45545 24.7553 4.84302 16.7772C3.33975 13.447 4.88844 10.2793 7.31572 7.94562C8.99792 6.33156 10.6416 5.03914 11.3646 2.5C14.5716 4.58333 15.0895 8.75 15.0895 10.8333C15.0895 10.8333 17.2824 8.71744 17.8186 7.96008Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
    <path
      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
    />
  </svg>
{/snippet}

Detached triggers

By default the trigger sits inside <Popover.Root>, right beside the content it opens, as at the top of this page.

When the two can't live together in the markup (say the button belongs in a toolbar but the panel elsewhere), detach them: render <Popover.Trigger> wherever it makes sense and connect it to the root with a shared handle from new Popover.Handle().

<script>
  const demoPopover = new Popover.Handle()
</script>

<Popover.Trigger handle={demoPopover}>Trigger</Popover.Trigger>

<Popover.Root handle={demoPopover}>...</Popover.Root>
<script lang="ts">
  import { Popover } from '@shardsui/svelte/popover'

  const demoPopover = new Popover.Handle()
</script>

<Popover.Trigger
  handle={demoPopover}
  class="flex size-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100"
  aria-label="Activity"
>
  {@render fireIcon()}
</Popover.Trigger>

<Popover.Root handle={demoPopover}>
  <Popover.Portal>
    <Popover.Positioner sideOffset={8}>
      <Popover.Popup
        class="flex max-w-72 origin-(--transform-origin) flex-col gap-1 rounded-lg bg-gray-50 p-3 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
      >
        <Popover.Arrow
          class="data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
        >
          {@render arrow()}
        </Popover.Arrow>
        <Popover.Title class="text-sm font-semibold">Activity</Popover.Title>
        <Popover.Description class="text-sm text-gray-600">
          12 days active this month.
        </Popover.Description>
      </Popover.Popup>
    </Popover.Positioner>
  </Popover.Portal>
</Popover.Root>

{#snippet fireIcon()}
  <svg viewBox="0 0 24 24" fill="none" class="size-4" aria-hidden="true">
    <path
      d="M17.8186 7.96008C18.9803 9.06824 19.4816 10.4992 19.6695 11.9928C20.8623 21.3865 8.45545 24.7553 4.84302 16.7772C3.33975 13.447 4.88844 10.2793 7.31572 7.94562C8.99792 6.33156 10.6416 5.03914 11.3646 2.5C14.5716 4.58333 15.0895 8.75 15.0895 10.8333C15.0895 10.8333 17.2824 8.71744 17.8186 7.96008Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
    <path
      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
    />
  </svg>
{/snippet}

Multiple triggers

One popover can answer to several triggers: drop multiple <Popover.Trigger> elements inside a single <Popover.Root>, or give the same handle to any number of detached triggers.

<Popover.Root>
  <Popover.Trigger>Trigger 1</Popover.Trigger>
  <Popover.Trigger>Trigger 2</Popover.Trigger>
  ...
</Popover.Root>
<script>
  const demoPopover = new Popover.Handle()
</script>

<Popover.Trigger handle={demoPopover}>Trigger 1</Popover.Trigger>

<Popover.Trigger handle={demoPopover}>Trigger 2</Popover.Trigger>

<Popover.Root handle={demoPopover}>...</Popover.Root>

When triggers share a popover, each one can hand the root its own data through the payload prop; read it back from the children snippet on <Popover.Root> to tailor what the panel shows. Pass a type argument to new Popover.Handle() to type the payload:

<script>
  const demoPopover = new Popover.Handle<{ text: string }>()
</script>

<Popover.Trigger handle={demoPopover} payload={{ text: 'Trigger 1' }}>Trigger 1</Popover.Trigger>

<Popover.Trigger handle={demoPopover} payload={{ text: 'Trigger 2' }}>Trigger 2</Popover.Trigger>

<Popover.Root handle={demoPopover}>
  {#snippet children({ payload })}
    <Popover.Portal>
      <Popover.Positioner sideOffset={8}>
        <Popover.Popup>
          <Popover.Title>Popover</Popover.Title>
          {#if payload !== undefined}
            <Popover.Description>
              This has been opened by {payload.text}
            </Popover.Description>
          {/if}
        </Popover.Popup>
      </Popover.Positioner>
    </Popover.Portal>
  {/snippet}
</Popover.Root>

Controlled mode with multiple triggers

To drive the popover from your own state — a button elsewhere on the page, a keyboard shortcut — bind open and handle onOpenChange on <Popover.Root>. With several triggers, give each one an id and add bind:triggerId to <Popover.Root>: each trigger publishes its own id when it opens the popover, and setting triggerId yourself anchors the popover to that trigger.

<script lang="ts">
  import { Popover } from '@shardsui/svelte/popover'

  const triggers = [
    { id: 'details', label: 'Details', icon: bookIcon },
    { id: 'activity', label: 'Activity', icon: progressIcon },
    { id: 'team', label: 'Team', icon: peopleIcon }
  ]

  const demoPopover = new Popover.Handle()

  let open = $state(false)
  let triggerId = $state<string | null>(null)
</script>

<div class="flex flex-wrap justify-center gap-2">
  {#each triggers as trigger (trigger.id)}
    <Popover.Trigger
      handle={demoPopover}
      id={trigger.id}
      class="flex size-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100"
      aria-label={trigger.label}
    >
      {@render trigger.icon()}
    </Popover.Trigger>
  {/each}

  <button
    type="button"
    class="flex h-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 px-3 text-sm font-normal text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100"
    onclick={() => {
      triggerId = 'activity'
      open = true
    }}
  >
    Open Activity
  </button>
</div>

<Popover.Root handle={demoPopover} bind:open bind:triggerId>
  <Popover.Portal>
    <Popover.Positioner
      class="h-(--positioner-height) w-(--positioner-width) max-w-(--available-width)"
      sideOffset={8}
    >
      <Popover.Popup
        class="relative flex h-(--popup-height,auto) w-(--popup-width,auto) max-w-72 origin-(--transform-origin) flex-col gap-1 rounded-lg bg-gray-50 p-3 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[transform,scale,opacity] duration-100 ease-out data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0"
      >
        <Popover.Arrow
          class="data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
        >
          {@render arrow()}
        </Popover.Arrow>
        {#if triggerId === 'details'}
          {@render detailsPanel()}
        {:else if triggerId === 'team'}
          {@render teamPanel()}
        {:else}
          {@render activityPanel()}
        {/if}
      </Popover.Popup>
    </Popover.Positioner>
  </Popover.Portal>
</Popover.Root>

{#snippet bookIcon()}
  <svg viewBox="0 0 24 24" fill="none" class="size-4" aria-hidden="true">
    <path
      d="M19.25 12V13.75C19.25 15.4069 17.9069 16.75 16.25 16.75H7C5.75736 16.75 4.75 17.7574 4.75 19C4.75 20.2426 5.75736 21.25 7 21.25H10M8.75 7H15.25M8.75 11H12.25M6.75 2.75H16.25C17.9069 2.75 19.25 4.09315 19.25 5.75V18.25C19.25 19.9069 17.9069 21.25 16.25 21.25H6.75C5.64543 21.25 4.75 20.3546 4.75 19.25V4.75C4.75 3.64543 5.64543 2.75 6.75 2.75Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

{#snippet progressIcon()}
  <svg viewBox="0 0 24 24" fill="none" class="size-4" aria-hidden="true">
    <circle cx="12" cy="12" r="9.25" stroke="currentColor" stroke-width="1.5" />
    <path
      d="M12 18.75C15.7279 18.75 18.75 15.7279 18.75 12C18.75 8.27208 15.7279 5.25 12 5.25V12H5.25C5.25 15.7279 8.27208 18.75 12 18.75Z"
      fill="currentColor"
    />
  </svg>
{/snippet}

{#snippet peopleIcon()}
  <svg viewBox="0 0 24 24" fill="none" class="size-4" aria-hidden="true">
    <path
      d="M15.75 6.5C15.75 8.57107 14.0711 10.25 12 10.25C9.92893 10.25 8.25 8.57107 8.25 6.5C8.25 4.42893 9.92893 2.75 12 2.75C14.0711 2.75 15.75 4.42893 15.75 6.5Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linejoin="round"
    />
    <path
      d="M11.9997 13.25C9.02123 13.25 6.67402 14.8039 5.43304 17.1121C4.59593 18.6691 6.02717 20.25 7.79494 20.25H16.2044C17.9722 20.25 19.4034 18.6691 18.5663 17.1121C17.3254 14.8039 14.9781 13.25 11.9997 13.25Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

{#snippet detailsPanel()}
  <Popover.Title class="m-0 text-sm font-semibold">Details</Popover.Title>
  <Popover.Description class="m-0 text-sm text-gray-600">
    Three sections with twelve items in total.
  </Popover.Description>
{/snippet}

{#snippet activityPanel()}
  <Popover.Title class="m-0 text-sm font-semibold">Activity</Popover.Title>
  <Popover.Description class="m-0 text-sm text-gray-600">
    Seven of twelve items done this week.
  </Popover.Description>
{/snippet}

{#snippet teamPanel()}
  <Popover.Title class="m-0 text-sm font-semibold">Team</Popover.Title>
  <div class="mt-1 flex flex-col gap-1 text-sm">
    <a href="/" class="text-gray-900 no-underline hover:underline">Milton Glaser</a>
    <a href="/" class="text-gray-900 no-underline hover:underline">Paula Scher</a>
    <a href="/" class="text-gray-900 no-underline hover:underline">Stefan Sagmeister</a>
  </div>
{/snippet}

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
    <path
      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
    />
  </svg>
{/snippet}

Animating the Popover

When one popover serves several triggers, it can glide from one to the next instead of snapping. Three things animate independently: its position, its size, and its contents.

Position and Size

Position lives on the Positioner, so transition its left, right, top, and bottom. Size lives on the Popup, so transition its width and height.

Content

The content itself can cross-fade when the active trigger changes. Wrap it in <Popover.Viewport>, which notices the switch and exposes a data-activation-direction attribute so the animation can lean toward the new trigger. Its value is a space-separated set of up to two tokens, one per axis — left or right for the horizontal axis, up or down for the vertical one (for example, right down). Match a single token with the ~= attribute selector, such as [data-activation-direction~='right'].

During a transition the viewport keeps both the incoming and outgoing content mounted, each in its own wrapper:

  • data-current — the content coming in, or the only content when nothing is transitioning. It also carries data-starting-style while it animates in.
  • data-previous — the content on its way out. It also carries data-ending-style while it animates out.

Target these to write the enter and exit animations.

<script lang="ts">
  import { Popover } from '@shardsui/svelte/popover'
  import type { Snippet } from 'svelte'

  const demoPopover = new Popover.Handle<Snippet>()

  const triggers = [
    { label: 'Details', icon: bookIcon, payload: detailsPanel },
    { label: 'Activity', icon: progressIcon, payload: activityPanel },
    { label: 'Team', icon: peopleIcon, payload: teamPanel }
  ]
</script>

<div class="flex gap-2">
  {#each triggers as trigger (trigger.label)}
    <Popover.Trigger
      handle={demoPopover}
      payload={trigger.payload}
      class="box-border flex size-8 items-center justify-center rounded-md border border-gray-200 bg-gray-50 text-gray-900 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100"
      aria-label={trigger.label}
    >
      {@render trigger.icon()}
    </Popover.Trigger>
  {/each}
</div>

<Popover.Root handle={demoPopover}>
  {#snippet children({ payload })}
    <Popover.Portal>
      <Popover.Positioner
        sideOffset={8}
        class="h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] duration-350 ease-out-quint data-instant:transition-none"
      >
        <Popover.Popup
          class="relative h-(--popup-height,auto) w-(--popup-width,auto) max-w-72 origin-(--transform-origin) rounded-lg bg-gray-50 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[width,height,opacity,scale] duration-350 ease-out-quint data-ending-style:scale-95 data-ending-style:opacity-0 data-instant:transition-none data-starting-style:scale-95 data-starting-style:opacity-0"
        >
          <Popover.Arrow
            class="flex transition-[left] duration-350 ease-out-quint data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
          >
            {@render arrow()}
          </Popover.Arrow>

          <Popover.Viewport
            class="relative size-full overflow-clip p-3 **:data-current:w-[calc(var(--popup-width)-1.5rem)] **:data-current:translate-x-0 **:data-current:opacity-100 **:data-current:transition-[translate,opacity] **:data-current:duration-[350ms,175ms] **:data-current:ease-out-quint **:data-previous:w-[calc(var(--popup-width)-1.5rem)] **:data-previous:translate-x-0 **:data-previous:opacity-100 **:data-previous:transition-[translate,opacity] **:data-previous:duration-[350ms,175ms] **:data-previous:ease-out-quint data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:-translate-x-1/2 data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:opacity-0 data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:translate-x-1/2 data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:opacity-0 data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:translate-x-1/2 data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:opacity-0 data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:-translate-x-1/2 data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:opacity-0"
          >
            {@render payload?.()}
          </Popover.Viewport>
        </Popover.Popup>
      </Popover.Positioner>
    </Popover.Portal>
  {/snippet}
</Popover.Root>

{#snippet bookIcon()}
  <svg viewBox="0 0 24 24" fill="none" class="size-4" aria-hidden="true">
    <path
      d="M19.25 12V13.75C19.25 15.4069 17.9069 16.75 16.25 16.75H7C5.75736 16.75 4.75 17.7574 4.75 19C4.75 20.2426 5.75736 21.25 7 21.25H10M8.75 7H15.25M8.75 11H12.25M6.75 2.75H16.25C17.9069 2.75 19.25 4.09315 19.25 5.75V18.25C19.25 19.9069 17.9069 21.25 16.25 21.25H6.75C5.64543 21.25 4.75 20.3546 4.75 19.25V4.75C4.75 3.64543 5.64543 2.75 6.75 2.75Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linecap="round"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

{#snippet progressIcon()}
  <svg viewBox="0 0 24 24" fill="none" class="size-4" aria-hidden="true">
    <circle cx="12" cy="12" r="9.25" stroke="currentColor" stroke-width="1.5" />
    <path
      d="M12 18.75C15.7279 18.75 18.75 15.7279 18.75 12C18.75 8.27208 15.7279 5.25 12 5.25V12H5.25C5.25 15.7279 8.27208 18.75 12 18.75Z"
      fill="currentColor"
    />
  </svg>
{/snippet}

{#snippet peopleIcon()}
  <svg viewBox="0 0 24 24" fill="none" class="size-4" aria-hidden="true">
    <path
      d="M15.75 6.5C15.75 8.57107 14.0711 10.25 12 10.25C9.92893 10.25 8.25 8.57107 8.25 6.5C8.25 4.42893 9.92893 2.75 12 2.75C14.0711 2.75 15.75 4.42893 15.75 6.5Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linejoin="round"
    />
    <path
      d="M11.9997 13.25C9.02123 13.25 6.67402 14.8039 5.43304 17.1121C4.59593 18.6691 6.02717 20.25 7.79494 20.25H16.2044C17.9722 20.25 19.4034 18.6691 18.5663 17.1121C17.3254 14.8039 14.9781 13.25 11.9997 13.25Z"
      stroke="currentColor"
      stroke-width="1.5"
      stroke-linejoin="round"
    />
  </svg>
{/snippet}

{#snippet detailsPanel()}
  <Popover.Title class="m-0 text-sm font-semibold">Details</Popover.Title>
  <Popover.Description class="m-0 text-sm text-gray-600">
    Three sections with twelve items in total.
  </Popover.Description>
{/snippet}

{#snippet activityPanel()}
  <Popover.Title class="m-0 text-sm font-semibold">Activity</Popover.Title>
  <Popover.Description class="m-0 text-sm text-gray-600">
    Seven of twelve items done this week.
  </Popover.Description>
{/snippet}

{#snippet teamPanel()}
  <Popover.Title class="m-0 text-sm font-semibold">Team</Popover.Title>
  <div class="mt-1 flex flex-col gap-1 text-sm">
    <a href="/" class="text-gray-900 no-underline hover:underline">Milton Glaser</a>
    <a href="/" class="text-gray-900 no-underline hover:underline">Paula Scher</a>
    <a href="/" class="text-gray-900 no-underline hover:underline">Stefan Sagmeister</a>
  </div>
{/snippet}

{#snippet arrow()}
  <svg width="20" height="10" viewBox="0 0 20 10" fill="none">
    <path
      d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
      class="fill-gray-50"
    />
    <path
      d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
      class="fill-gray-200"
    />
    <path
      d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
    />
  </svg>
{/snippet}

API reference

Root

Groups all parts of the popover. Doesn't render its own HTML element.

PropTypeDefault

Trigger

A button that opens the popover. Renders a <button> element.

PropTypeDefault
AttributeDescription
data-popup-openPresent while the popover is open from this trigger.
data-pressedPresent while the popover is open from this trigger, opened by a press rather than hover.
data-disabledPresent when the trigger is disabled.

Backdrop

An overlay displayed beneath the popup. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popover is open.
data-closedPresent when the popover is closed.
data-starting-stylePresent when the backdrop is animating in.
data-ending-stylePresent when the backdrop is animating out.

Portal

A portal that moves the popup out to <body>, clear of ancestor clipping and stacking. Renders a <div> element.

PropTypeDefault

Positioner

Positions the popover against the trigger. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-anchor-hiddenPresent when the anchor is hidden.
data-instantPresent when animations should be instant.
CSS VariableDescription
--available-widthAvailable width between the anchor and the viewport edge.
--available-heightAvailable height between the anchor and the viewport edge.
--anchor-widthWidth of the anchor element.
--anchor-heightHeight of the anchor element.
--transform-originTransform origin for scale animations.
--positioner-widthThe positioner element's width. Set while a <Popover.Viewport> morphs content.
--positioner-heightThe positioner element's height. Set while a <Popover.Viewport> morphs content.

Popup

A container for the popover contents. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popover is open.
data-closedPresent when the popover is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-starting-stylePresent when the popover is animating in.
data-ending-stylePresent when the popover is animating out.
data-instantPresent when animations should be instant.
CSS VariableDescription
--popup-widthThe popup element's width. Set while a <Popover.Viewport> morphs content.
--popup-heightThe popup element's height. Set while a <Popover.Viewport> morphs content.

Arrow

Displays an element positioned against the anchor. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-openPresent when the popup is open.
data-closedPresent when the popup is closed.
data-sideWhich side of the anchor the popup is on.
data-alignHow the popup is aligned relative to the side.
data-uncenteredPresent when the arrow cannot be centered.

Title

A heading that labels the popover. Renders an <h2> element.

PropTypeDefault

Description

A paragraph with additional information about the popover. Renders a <p> element.

PropTypeDefault

Close

A button that closes the popover. Renders a <button> element.

PropTypeDefault

Viewport

A viewport for displaying content transitions. This component is only required if one popup can be opened by multiple triggers, its content changes based on the trigger, and switching between them is animated. Renders a <div> element.

PropTypeDefault
AttributeDescription
data-activation-directionDirection the popup was activated from. A horizontal and a vertical token separated by a space, e.g. right down; either can be empty, so match one with [data-activation-direction~='right'].
data-transitioningPresent while a content transition is in progress.
data-currentApplied to the wrapper of the current content.
data-previousApplied to the wrapper of the outgoing content, present only during a transition.
data-instantPresent when animations should be instant. Value is 'click', 'dismiss', 'focus', or 'trigger-change'.
data-starting-styleApplied to the data-current wrapper while the incoming content is animating in.
data-ending-styleApplied to the data-previous wrapper while the outgoing content is animating out.
CSS VariableDescription
--popup-widthWidth of the outgoing popup content, set on the data-previous container. Use it to freeze the popup size while the content morphs.
--popup-heightHeight of the outgoing popup content, set on the data-previous container.

The Viewport is optional — reach for it only when a single popup is opened by multiple triggers, its content differs per trigger, and the switch between them is animated. When used, set width: var(--positioner-width) and height: var(--positioner-height) on the Positioner so its box is frozen to the measured size during the transition; otherwise content-driven resizing can make the popup thrash or flip to another side.

Handle

Connects a <Popover.Root> with detached <Popover.Trigger> components, and controls the popover imperatively. Pass a type argument to type the payload.

const popover = new Popover.Handle<Payload>()
MemberType