Skip to main content

Popover

Show anchored interactive content from click or hover.

Use Popover for anchored interactive content that does not need the structure of a full dialog. Use Tooltip for brief, non-interactive hints.

Basic usage#

import { Button, Popover } from 'moraine'

export function Example() {
  return (
    <Popover>
      <Popover.Trigger as={Button}>Details</Popover.Trigger>
      <Popover.Content>More information</Popover.Content>
    </Popover>
  )
}

Playground#

Props
Slots

Anatomy#

Popover [component; no DOM]
├── Popover.Trigger [part; slot=trigger]
├── Popover.Close [part]
└── positioner [internal]
    └── Popover.Content [part; slot=content]
        └── body [slot]

Content is portaled inside an internal positioning wrapper.

Usage#

Trigger modes and state#

Render the trigger with the supplied props. Click mode is the default; hover mode also responds to focus and blur and can use open and close delays. Use controlled open state only when the application needs to coordinate visibility.

import { Avatar, Badge, Button, Popover } from 'moraine'

export function TriggerModes() {
  return (
    <div class="flex flex-wrap gap-4 items-center">
      <Popover>
        <Popover.Trigger as={Button} variant="outline" leading="i-lucide:sliders-horizontal">
          Click to Filter
        </Popover.Trigger>
        <Popover.Content>
          <div class="p-3 w-64 space-y-2">
            <h4 class="text-xs text-foreground font-semibold">Filter Deployments</h4>
            <p class="text-xs text-muted-foreground">
              Click-triggered popover remains open during interactive selections.
            </p>
            <div class="pt-1 flex gap-1.5">
              <Badge variant="outline" size="sm">
                Production
              </Badge>
              <Badge variant="outline" size="sm">
                Staging
              </Badge>
              <Badge variant="outline" size="sm">
                Canary
              </Badge>
            </div>
          </div>
        </Popover.Content>
      </Popover>

      <Popover mode="hover" openDelay={150} closeDelay={100}>
        <Popover.Trigger as={Button} variant="ghost" leading="i-lucide:user">
          Hover for Profile
        </Popover.Trigger>
        <Popover.Content>
          <div class="p-3 w-56 space-y-2">
            <div class="flex gap-2 items-center">
              <Avatar text="AR" size="sm" />
              <div>
                <p class="text-xs text-foreground font-semibold">Alex Rivera</p>
                <p class="text-[0.7rem] text-muted-foreground">Core Maintainer</p>
              </div>
            </div>
            <p class="text-xs text-muted-foreground">
              Hover-triggered info preview with automatic delay timers.
            </p>
          </div>
        </Popover.Content>
      </Popover>
    </div>
  )
}

Dismissal and accessibility#

Escape and outside interaction close a dismissible popover. Give Popover.Content an accessible name with ariaLabel when it has no visible label.

modal defaults to false. With modal={true} and a composed Popover.Close, focus stays inside the content, outside content is hidden from assistive technology, and body scroll is locked. Without a Close, the popover stays non-modal so users retain a dismissal route.

modal={false} does not disable Escape or outside dismissal; use dismissible for that. preventScroll can lock body scroll independently of modal.

import { Button, Popover } from 'moraine'

export function DismissalUsage() {
  return (
    <Popover>
      <Popover.Trigger as={Button} variant="outline" leading="i-lucide:sliders-horizontal">
        Filter
      </Popover.Trigger>
      <Popover.Content ariaLabel="Filter settings">
        <div class="text-xs p-3 w-48 space-y-2">
          <p class="text-foreground font-medium">Filter Settings</p>
          <p class="text-muted-foreground">Press Escape or click outside to dismiss.</p>
        </div>
      </Popover.Content>
    </Popover>
  )
}

Keyboard interaction#

Keyboard behavior depends on the trigger element and whether the popover is modal. Escape requests dismissal when the popover is dismissible; focusable controls inside non-modal content participate in normal document focus order. Modal content contains focus.

Examples#

Hover mode#

import { Avatar, Badge, Button, Popover } from 'moraine'

export function HoverMode() {
  return (
    <div class="flex flex-wrap gap-3 items-center">
      <Popover mode="hover" openDelay={180} closeDelay={120}>
        <Popover.Trigger as={Button} variant="outline" leading="i-lucide:user">
          Hover for Author Card
        </Popover.Trigger>
        <Popover.Content>
          <div class="p-4 rounded-xl bg-card max-w-xs space-y-3">
            <div class="flex items-start justify-between">
              <Avatar text="AM" alt="Alex Morgan" size="lg" />
              <Button size="xs" variant="default">
                Follow
              </Button>
            </div>

            <div>
              <div class="flex gap-1.5 items-center">
                <h4 class="text-sm font-semibold">Alex Morgan</h4>
                <Badge variant="outline" size="sm">
                  Author
                </Badge>
              </div>
              <p class="text-xs text-muted-foreground font-mono">@alex.morgan</p>
            </div>

            <p class="text-xs text-foreground">
              Building accessible, high-performance UI primitives for SolidJS and web standards.
            </p>

            <div class="text-xs text-muted-foreground pt-1 border-t border-border flex gap-3">
              <span>
                <strong class="text-foreground font-semibold">1.4k</strong> followers
              </span>
              <span>
                <strong class="text-foreground font-semibold">328</strong> following
              </span>
            </div>
          </div>
        </Popover.Content>
      </Popover>
    </div>
  )
}

Dismiss control#

import { Button, Popover } from 'moraine'
import { createSignal } from 'solid-js'

export function DismissControl() {
  const [preventedCloseCount, setPreventedCloseCount] = createSignal(0)

  return (
    <div class="flex flex-wrap gap-3 items-center">
      <Popover
        defaultOpen
        dismissible={false}
        onClosePrevent={() => setPreventedCloseCount((value) => value + 1)}
      >
        <Popover.Trigger as={Button} variant="secondary">
          Try close me
        </Popover.Trigger>
        <Popover.Content>
          <div class="p-3 space-y-1">
            <p class="text-sm font-medium">Persistent popover</p>
            <p class="text-xs text-muted-foreground">
              Prevented close attempts: {preventedCloseCount()}
            </p>
          </div>
        </Popover.Content>
      </Popover>
    </div>
  )
}

Attributes#

Attributes
data-closedSlot: popover-trigger, popover-contentDescription: Present when disclosure or transition content is closed.
data-disabledSlot: popover-triggerDescription: Present when the component, slot, or item is disabled.
data-expandedSlot: popover-trigger, popover-contentDescription: Present when the panel, accordion, or menu is expanded.
data-alignSlot: popover-contentDescription: Stores the resolved alignment of positioned content.
data-sideSlot: popover-contentDescription: Stores the resolved floating or drawer content side.

Props#

Popover#

Prop

Trigger#

Renders a <button> element by default.

Prop

Content#

Props for the Popover component. Renders a <div> element by default.

Prop

Close#

Renders a <button> element by default.

Prop