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.
<.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.
<.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.
Switch workspace
overflow
hidden
<.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
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