Skip to main content
Urbicon UI

Popover

Floating content panel anchored to a trigger with precise positioning, portal rendering, and size syncing.

Playground

Choose an option…
Size Style variant (tailwind-variants)
Offset
4
<script lang="ts">
  import { ChevronDownIcon, Popover } from '@urbicon-ui/blocks';
</script>

<Popover
  offsetDistance={4}
  placement="bottom-start"
>
  {#snippet trigger()}
    <div
      class="bg-surface-base border-border-default hover:border-border-emphasis flex w-80 cursor-pointer items-center justify-between rounded-lg border px-3 py-2.5 text-sm transition-colors"
    >
      <span class="text-text-secondary">Choose an option…</span>
      <ChevronDownIcon class="text-text-tertiary h-4 w-4" />
    </div>
  {/snippet}

  <div class="divide-border-subtle divide-y">
    {#each ['Design tokens', 'Component variants', 'Documentation'] as option (option)}
      <div
        class="text-text-primary hover:bg-surface-hover cursor-pointer px-3 py-2 transition-colors first:rounded-t-md last:rounded-b-md"
      >
        {option}
      </div>
    {/each}
  </div>
</Popover>

01 When to use

Popover is a floating panel anchored to a trigger element. Use it for contextual surfaces — action menus, date pickers, inline help — that should appear next to the element the user just interacted with. Placement comes from the library's built-in zero-dependency floating engine (flip, shift, offset), with native popover="auto" for the open/close lifecycle. Not modal.

Pick a different overlay if you need:

  • A modal sheet that opens from the edge of the viewport (focus trap, backdrop) → Drawer.
  • A hover-only description tied to aria-describedbyTooltip.
  • A list of selectable actions or items with full keyboard semantics → Menu.
  • A centered, blocking modal (confirmation, short form) → Dialog.

Inside a paragraph

A popover whose trigger sits in flowing text — a citation marker in a sentence, a term with a definition — needs inline. By default the trigger wrapper and the panel are both <div>s, and a <div> start tag closes an open <p> however deeply it is nested. On a server-rendered page the browser repairs that by ending the paragraph early, so the DOM it builds no longer matches the component tree and hydration reports a mismatch.

inline makes the wrapper a <span> and withholds the panel from the server render, adding it on mount. The cost is that the panel's content is absent from the prerendered HTML, so a non-rendering crawler never sees it — which is why it is opt-in rather than the default. There is nothing to see in the playground: the difference is entirely in the server output.

02 Examples

Placements

The popover auto-flips when the preferred side runs out of space.
{#each [{ label: 'Top', placement: 'top' }, { label: 'Top Start', placement: 'top-start' }, { label: 'Top End', placement: 'top-end' }, { label: 'Bottom', placement: 'bottom' }, { label: 'Left', placement: 'left' }, { label: 'Right', placement: 'right' }] as const as { label, placement } (placement)}
  <Popover {placement}>
    {#snippet trigger()}
      <Button variant="outlined" size="sm">{label}</Button>
    {/snippet}
    <div class="text-text-secondary px-3 py-2 whitespace-nowrap">
      Placed at <span class="text-text-primary font-medium">{placement}</span>
    </div>
  </Popover>
{/each}

Rich Content – User Profile

A popover is a surface, not just a tooltip — it can carry structured content, avatars and its own actions, and the focus order follows the markup you put inside.
<Popover placement="bottom-end">
  {#snippet trigger()}
    <div class="avatar-trigger">JD</div>
  {/snippet}
  {#snippet children()}
    <div class="w-64">
      <!-- profile card content -->
    </div>
  {/snippet}
</Popover>

Controlled State

Bind the open state to react to or drive popover visibility from outside.
<script>
  let open = $state(false);
</script>

<Popover bind:open>
  {#snippet trigger()}
    <Button>{open ? 'Viewing' : 'View'} Status</Button>
  {/snippet}
  {#snippet children()}
    <div class="p-3">...</div>
    <Button onclick={() => open = false}>Dismiss</Button>
  {/snippet}
</Popover>

03 Customization

Gradient Action Panel

Override the panel surface with slotClasses for a branded popover.
<Popover
  slotClasses={{
    base: 'bg-linear-to-br from-violet-600 to-indigo-700 border-none shadow-xl shadow-violet-500/20 text-white'
  }}
  size="md"
>
  {#snippet trigger()}
    <Button intent="primary">Upgrade Plan</Button>
  {/snippet}
  <div class="w-64 p-4">
    <div class="mb-1 text-sm font-bold text-white">Go Premium</div>
    <p class="mb-3 text-xs leading-relaxed text-white/75">
      Unlock advanced analytics, priority support, and unlimited projects.
    </p>
    <Button
      unstyled
      class="w-full rounded-lg bg-white px-3 py-2 text-center text-sm font-semibold text-violet-700 transition-all hover:bg-white/90"
    >
      Start Free Trial
    </Button>
  </div>
</Popover>

Glass Morphism (unstyled)

Strip all defaults and rebuild with a translucent glass aesthetic.
<Popover
  unstyled
  class="w-56 rounded-2xl border border-white/20 bg-white/10 p-4 text-white shadow-2xl backdrop-blur-xl"
>
  {#snippet trigger()}
    <Button
      unstyled
      class="rounded-full border border-white/30 bg-white/10 px-5 py-2.5 text-sm font-medium text-white backdrop-blur-sm transition-all hover:bg-white/20"
    >
      Quick Actions
    </Button>
  {/snippet}
  <div class="space-y-1">
    {#each ['New Project', 'Import Data', 'Invite Team'] as action (action)}
      <div
        class="cursor-pointer rounded-lg px-3 py-2 text-sm text-white/90 transition-colors hover:bg-white/10"
      >
        {action}
      </div>
    {/each}
  </div>
</Popover>

A branded panel that several popovers share belongs in a BlocksProvider preset (presets.Popover), applied via preset — see Customization.

04 Accessibility

ARIA Attributes

The trigger wrapper sets aria-haspopup="dialog" and aria-expanded reflecting the current open state. The floating panel receives role="dialog" by default. aria-modal is passed through as an attribute only — the popover never traps focus, so leave it unset and reach for Dialog when a flow is genuinely modal.

Keyboard

Enter / Space toggle the popover when the trigger is focused. Escape closes the popover and returns focus to the trigger. Tab moves through focusable content inside the popover.

Click Outside

Clicking outside the popover or its trigger closes it automatically. Set closeOnClickOutside to false to pin the popover open until you toggle open yourself; the onClickOutside callback fires after an outside click has dismissed it.

Focus Management

The popover never traps focus — it is a non-modal surface, and Tab moves on past its content. Closing with Escape returns focus to the trigger. For forms or critical actions that must hold focus until dismissed, use Dialog or ConfirmDialog instead.

Motion & Reduced Motion

The panel fades and scales in over the --blocks-popover-duration / --blocks-popover-easing tokens (150 ms by default, CSS-native via @starting-style and discrete transitions — Menu inherits it). Override per instance with transitionDuration / transitionEasing; under prefers-reduced-motion both collapse to near-instant automatically. With unstyled, rebuild motion on the panel's data-state attribute.

05 API Reference

26 props
26 props 1 required
Prop
Type
Default
Description

06 Types

Local type definitions used by this component.

5 types
Name
Kind
Category
Used by
Description

07 Installation

Import

import { Popover } from '@urbicon-ui/blocks';