Components Popover

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

Popover

A panel anchored to a trigger, opened on click and dismissed on click-away or Escape. Built on the same CSS technique as the dropdown, so there are no JavaScript dependencies. An opt-in top_layer mode renders in the browser's native top layer to escape clipped containers.
Basic

The :trigger slot renders inside a button. Put anything in the panel - a form, a menu, rich content.

heex
<.popover placement="bottom-start" panel_class="w-80">
  <:trigger>
    <.icon name="hero-adjustments-horizontal" class="w-4 h-4 mr-2" /> Dimensions
  </:trigger>
  <div class="space-y-3">
    <div>
      <.h5 class="mb-1" no_margin>Dimensions</.h5>
      <.p class="text-sm text-gray-500 dark:text-gray-400" no_margin>
        Set the dimensions for the layer.
      </.p>
    </div>
    <div class="grid grid-cols-2 items-center gap-2">
      <label class="text-sm">Width</label>
      <input type="text" value="100%" class="pc-text-input" />
      <label class="text-sm">Max. width</label>
      <input type="text" value="300px" class="pc-text-input" />
      <label class="text-sm">Height</label>
      <input type="text" value="25px" class="pc-text-input" />
    </div>
  </div>
</.popover>
Placement

placement takes a side (top, bottom, left, right) with an optional -start or -end alignment, twelve positions in all. The panel width is clamped to the viewport.

heex
<.popover placement="top">
  <:trigger>Top</:trigger>
  <p class="text-sm">Opens above, centered on the trigger.</p>
</.popover>
<.popover placement="right-start">
  <:trigger>Right start</:trigger>
  <p class="text-sm">Opens to the right, aligned to the trigger's top edge.</p>
</.popover>
Escaping clipped containers

A popover inside a container with overflow: hidden (a collapsed sidebar, a table cell, a modal) gets clipped by default. Pass top_layer and the panel renders in the browser’s native top layer via the HTML popover attribute, so it overlays everything, unclipped. Here an account switcher opens out of a narrow, clipped rail. The browser handles open, close and light-dismiss; a small hook positions it and keeps it on screen.

overflow
hidden

heex
<.popover top_layer placement="right-start" panel_class="w-64">
  <:trigger>
    <.avatar size="sm" name="Jane Doe" random_color />
  </:trigger>
  <div class="space-y-1">
    <.p class="px-2 text-xs font-semibold uppercase text-gray-400" no_margin>Switch workspace</.p>
    <button class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-gray-100 dark:hover:bg-gray-700">
      <.avatar size="xs" name="Acme Inc" random_color /> Acme Inc
    </button>
    <button class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-gray-100 dark:hover:bg-gray-700">
      <.avatar size="xs" name="Petal Labs" random_color /> Petal Labs
    </button>
    <div class="my-1 border-t border-gray-200 dark:border-gray-700"></div>
    <button class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-700">
      <.icon name="hero-arrow-right-start-on-rectangle" class="w-4 h-4" /> Sign out
    </button>
  </div>
</.popover>
Properties
elixir
          attr :id, :string           # panel id; autogenerated if not set
attr :placement, :string    # side + optional -start/-end, e.g. "bottom", "right-start"
attr :top_layer, :boolean   # render in the browser top layer to escape clipped containers
attr :class, :any           # extra classes for the wrapper
attr :trigger_class, :any   # extra classes for the trigger button
attr :panel_class, :any     # extra classes for the panel
attr :on_close, JS          # extra commands to run on close (anchored mode)
attr :rest, :global

slot :trigger, required: true      # trigger content, rendered inside a button
slot :inner_block, required: true  # panel content