Skip to content
Docs

Popover

Displays rich content in a portal, triggered by a button.

Displays rich content in a portal, triggered by a button.

<Popover.root id="popover-primitive">
  <Popover.trigger
    id="popover-trigger"
    content_id="popover-content"
    class="DemoIconButton"
    aria-label="Update dimensions"
  >
    <svg
      width="15"
      height="15"
      viewBox="0 0 15 15"
      fill="none"
      aria-hidden="true"
    >
      <path
        d="M5.5 3v9M9.5 3v9M3 5.5h9M3 9.5h9"
        stroke="currentColor"
        stroke-width="1.5"
        stroke-linecap="round"
      />
    </svg>
  </Popover.trigger>
  <Popover.content
    id="popover-content"
    class="DemoPopoverContent"
    side_offset={5}
  >
    <div style="display: flex; flex-direction: column; gap: 10px;">
      <p
        class="DemoText"
        style="margin-bottom: 10px;"
      >
        Dimensions
      </p>
      <fieldset class="DemoFieldset">
        <label
          class="DemoLabel"
          for="popover-width"
        >
          Width
        </label>
        <input
          class="DemoInput"
          id="popover-width"
          value="100%"
        />
      </fieldset>
      <fieldset class="DemoFieldset">
        <label
          class="DemoLabel"
          for="popover-maxWidth"
        >
          Max. width
        </label>
        <input
          class="DemoInput"
          id="popover-maxWidth"
          value="300px"
        />
      </fieldset>
      <fieldset class="DemoFieldset">
        <label
          class="DemoLabel"
          for="popover-height"
        >
          Height
        </label>
        <input
          class="DemoInput"
          id="popover-height"
          value="25px"
        />
      </fieldset>
      <fieldset class="DemoFieldset">
        <label
          class="DemoLabel"
          for="popover-maxHeight"
        >
          Max. height
        </label>
        <input
          class="DemoInput"
          id="popover-maxHeight"
          value="none"
        />
      </fieldset>
    </div>
    <Popover.close
      class="DemoPopoverClose"
      aria-label="Close"
    >
      ×
    </Popover.close>
    <Popover.arrow class="DemoPopoverArrow" />
  </Popover.content>
</Popover.root>
Can be controlled or uncontrolled.
Customize side, alignment, offsets, collision handling.
Optionally render a pointing arrow.
Focus is fully managed and customizable.
Supports modal and non-modal modes.
Dismissing and layering behavior is highly customizable.

Anatomy

Import all parts and piece them together.

<Popover.root id="…">
  <Popover.trigger content_id="…" />
  <Popover.anchor />
  <Popover.portal id="…">
    <Popover.content id="…">
      <Popover.close />
      <Popover.arrow />
    </Popover.content>
  </Popover.portal>
</Popover.root>

Anatomy

Root
Contains all the parts of a popover.
Trigger
The button that toggles the popover. Set `content_id` to the content element's `id`. By default, content positions against the trigger.
Anchor
An optional element to position content against instead of the trigger.
Portal
When used, portals the content part into the target (default `body`).
Content
The component that pops out when the popover is open.
Arrow
An optional arrow rendered inside `Popover.content`.
Close
The button that closes an open popover.

API Reference

Root

Contains all the parts of a popover.

Use open with on_open_change for controlled open state in LiveView:

<Popover.root id="dims-popover" open={@open} on_open_change="popover_open_change">
  …
</Popover.root>
def handle_event("popover_open_change", %{"open" => open}, socket) do
  {:noreply, assign(socket, :open, open)}
end
Prop Type Default Description
default_open boolean false
id string
modal boolean false
on_open_change string nil
open boolean false
default_open
Type boolean Default false
id
Type string Default
modal
Type boolean Default false
on_open_change
Type string Default nil
open
Type boolean Default false

Data attributes

Attribute Values Description
[data-state] open | closed Reflects whether the popover is open.
[data-modal] Present when modal Present when `modal` is true.

Trigger

The button that toggles the popover. By default, Popover.content positions itself against the trigger.

Prop Type Default Description
as string "button"
content_id string nil
id string nil
as
Type string Default "button"
content_id
Type string Default nil
id
Type string Default nil

Data attributes

Attribute Values Description
[data-state] open | closed Reflects whether the popover is open.

Anchor

An optional element to position Popover.content against. If omitted, content positions alongside Popover.trigger.

Prop Type Default Description
id string nil
id
Type string Default nil

Portal

When used, portals the content part into the target (default body).

Prop Type Default Description
class any nil
container string "div"
id string
target string "body"
class
Type any Default nil
container
Type string Default "div"
id
Type string Default
target
Type string Default "body"

Content

The component that pops out when the popover is open.

Prop Type Default Description
align string "center"
id string nil
side string "bottom"
side_offset integer 8
align
Type string Default "center"
id
Type string Default nil
side
Type string Default "bottom"
side_offset
Type integer Default 8

Data attributes

Attribute Values Description
[data-state] open | closed Reflects whether the popover is open.
[data-side] top | right | bottom | left Preferred side relative to the anchor.
[data-align] start | center | end Alignment along the side.

Arrow

An optional arrow element to render alongside the popover. Must be rendered inside Popover.content.

Prop Type Default Description
height integer 5
id string nil
width integer 10
height
Type integer Default 5
id
Type string Default nil
width
Type integer Default 10

Close

The button that closes an open popover.

Prop Type Default Description
as string "button"
id string nil
as
Type string Default "button"
id
Type string Default nil

Examples

Constrain the content size

You may want to constrain the width or height of the content so it does not exceed the viewport:

<Popover.content id="popover-content" class="DemoPopoverContent" side_offset={5}>
  …
</Popover.content>
.DemoPopoverContent {
  width: 260px;
  max-width: calc(100vw - 16px);
  max-height: calc(100vh - 16px);
  overflow-y: auto;
}

Origin-aware animations

Animate with CSS against data-state — see the animation guide:

.DemoPopoverContent {
  animation-duration: 0.5s;
  animation-timing-function: ease-out;
}
.DemoPopoverContent[data-state="open"] {
  animation-name: scaleIn;
}

@keyframes scaleIn {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: scale(1); }
}

Collision-aware animations

Popover.content exposes data-side and data-align for direction-aware animations based on the configured placement:

<Popover.content id="popover-content" class="DemoPopoverContent" side="bottom" align="center">
  …
</Popover.content>
.DemoPopoverContent {
  animation-duration: 0.6s;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.DemoPopoverContent[data-side="top"] {
  animation-name: slideUp;
}
.DemoPopoverContent[data-side="bottom"] {
  animation-name: slideDown;
}

@keyframes slideDown {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}

@keyframes slideUp {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}

With custom anchor

Anchor content to another element when the trigger should not be the positioning reference:

<Popover.root id="anchor-popover">
  <Popover.anchor class="DemoPopoverRow">
    Row as anchor
    <Popover.trigger id="anchor-popover-trigger" content_id="anchor-popover-content" class="DemoButton">
      Trigger
    </Popover.trigger>
  </Popover.anchor>
  <Popover.content id="anchor-popover-content" class="DemoPopoverContent">
    …
  </Popover.content>
</Popover.root>
.DemoPopoverRow {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 20px;
  background: var(--mauve-3);
  border-radius: 6px;
}

Accessibility

Adheres to the Dialog WAI-ARIA design pattern.

Keyboard Interactions

Keyboard Interactions

Key Description
Space Opens/closes the popover.
Enter Opens/closes the popover.
Tab Moves focus to the next focusable element.
Shift + Tab Moves focus to the previous focusable element.
Escape Closes the popover and moves focus to `Popover.trigger`.

Custom APIs

Create your own API by abstracting the primitive parts into your own component.

Abstract the arrow and set default configuration

This example abstracts Popover.arrow and sets a default side_offset.

Usage

<.popover id="settings-popover">
  <:trigger>Popover trigger</:trigger>
  <:content>Popover content</:content>
</.popover>

Implementation

slot :trigger, required: true
slot :content, required: true
attr :id, :string, required: true

def popover(assigns) do
  ~H"""
  <Popover.root id={@id}>
    <Popover.trigger id={"#{@id}-trigger"} content_id={"#{@id}-content"} class="DemoButton">
      {render_slot(@trigger)}
    </Popover.trigger>
    <Popover.content id={"#{@id}-content"} class="DemoPopoverContent" side_offset={5}>
      {render_slot(@content)}
      <Popover.arrow class="DemoPopoverArrow" />
    </Popover.content>
  </Popover.root>
  """
end