---
title: Popover
description: Show anchored interactive content from click or hover.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# 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](https://moraine.subf.dev/components/tooltip.md) for brief, non-interactive hints.

## Basic usage

```tsx
import { Button, Popover } from 'moraine'

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

## Anatomy

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

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

```tsx
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

```tsx
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

```tsx
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 | Slot | Description |
| --- | --- | --- |
| `data-closed` | `popover-trigger`, `popover-content` | Present when disclosure or transition content is closed. |
| `data-disabled` | `popover-trigger` | Present when the component, slot, or item is disabled. |
| `data-expanded` | `popover-trigger`, `popover-content` | Present when the panel, accordion, or menu is expanded. |
| `data-align` | `popover-content` | Stores the resolved alignment of positioned content. |
| `data-side` | `popover-content` | Stores the resolved floating or drawer content side. |

## Props

### Popover

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| align | 'start' \| 'center' \| 'end' \| undefined | 'center' | Alignment along the cross axis. |
| closeDelay | number \| undefined | 100 | Delay in milliseconds before closing in hover mode. |
| defaultOpen | boolean \| undefined | false | Initial open state when uncontrolled. |
| disabled | boolean \| undefined | false | Whether trigger interactions and content rendering are disabled. |
| dismissible | boolean \| undefined | true | Whether outside interaction and Escape dismiss the content. |
| forceMount | boolean \| undefined | false | Whether content remains mounted while closed. |
| id | string \| undefined | — | Unique identifier used to derive the content id. |
| modal | boolean \| undefined | false | Whether the content traps focus, hides outside content from assistive technology, prevents<br>the native default action of outside pointer events, and locks body scroll.<br>This is enabled only when Popover.Content composes Popover.Close; otherwise the Popover<br>remains non-modal so assistive-technology users retain a dismissal route. Outside and<br>Escape dismissal remain controlled by `dismissible`. |
| mode | Mode \| undefined | 'click' | Interaction mode for triggering the popover. |
| onClosePrevent | (() => void) \| undefined | — | Called when a dismissal attempt is blocked. |
| onOpenChange | ((open: boolean) => void) \| undefined | — | Called whenever the open state changes. |
| open | boolean \| undefined | — | Controlled open state. |
| openDelay | number \| undefined | 100 | Delay in milliseconds before opening in hover mode. |
| placement | 'top' \| 'right' \| 'bottom' \| 'left' \| undefined | 'bottom' | Preferred content placement relative to the trigger. |
| preventScroll | boolean \| undefined | false | Whether body scroll should be locked while the content is present. |
| children | JSX.Element \| undefined | — | Composed trigger and content primitives. |
| classes | Classes \| undefined | — | Family slot class defaults for this Popover instance. |
| styles | Styles \| undefined | — | Family slot style defaults for this Popover instance. |

### Popover.Trigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| disabled | boolean \| undefined | — | Whether this trigger is disabled. |
| as | T \| undefined | 'button' | Element or component to render as. |
| children | JSX.Element \| undefined | — | Trigger label and visual content. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Popover.Content

Props for the Popover component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| ariaLabel | string \| undefined | — | — |
| children | JSX.Element \| undefined | — | Body content. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | ContentClasses \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | ContentStyles \| undefined | — | Family slot style defaults for this instance. |

### Popover.Close

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| disabled | boolean \| undefined | — | Whether this trigger is disabled. |
| as | T \| undefined | 'button' | Element or component to render as. |
| children | JSX.Element \| undefined | — | Trigger label and visual content. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
