Popover
A popover is a floating panel that displays content over other page elements.
Built on the Zag.js popover machine, anchored to its trigger through Floating UI.
Usage
Dimensions
Set the dimensions for the layer.
<twig:ui:Popover>
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" {{ ...popover_trigger_attrs }}>Open popover</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-72">
<div class="grid gap-1">
<p class="text-sm leading-none font-medium text-neutral-950 dark:text-neutral-50">Dimensions</p>
<p class="text-sm text-neutral-500 dark:text-neutral-400">Set the dimensions for the layer.</p>
</div>
<div class="grid grid-cols-3 items-center gap-3">
<label for="usage-popover-width" class="text-neutral-600 dark:text-neutral-400">Width</label>
<input
id="usage-popover-width"
value="100%"
class="col-span-2 h-8 rounded-md border border-neutral-200 bg-transparent px-2 dark:border-neutral-700"
/>
<label for="usage-popover-height" class="text-neutral-600 dark:text-neutral-400">Height</label>
<input
id="usage-popover-height"
value="25px"
class="col-span-2 h-8 rounded-md border border-neutral-200 bg-transparent px-2 dark:border-neutral-700"
/>
</div>
</twig:ui:Popover:Content>
</twig:ui:Popover>
Examples
Open by default
<twig:ui:Popover :open="true">
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" {{ ...popover_trigger_attrs }}>Open popover</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-56">
Open on initial render.
</twig:ui:Popover:Content>
</twig:ui:Popover>
Placement
placement="top-start"
placement="top"
placement="top-end"
placement="right-start"
placement="right"
placement="right-end"
placement="bottom-end"
placement="bottom"
placement="bottom-start"
placement="left-end"
placement="left"
placement="left-start"
{% set placements = [
{value: 'top-start', col: 2, row: 1},
{value: 'top', col: 3, row: 1},
{value: 'top-end', col: 4, row: 1},
{value: 'right-start', col: 5, row: 2},
{value: 'right', col: 5, row: 3},
{value: 'right-end', col: 5, row: 4},
{value: 'bottom-end', col: 4, row: 5},
{value: 'bottom', col: 3, row: 5},
{value: 'bottom-start', col: 2, row: 5},
{value: 'left-end', col: 1, row: 4},
{value: 'left', col: 1, row: 3},
{value: 'left-start', col: 1, row: 2},
] %}
<div class="mx-auto grid w-lg grid-cols-5 grid-rows-5 gap-3">
{% for p in placements %}
<div class="flex items-center justify-center" style="grid-column: {{ p.col }}; grid-row: {{ p.row }};">
<twig:ui:Popover placement="{{ p.value }}">
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" size="sm" {{ ...popover_trigger_attrs }} class="whitespace-nowrap px-2 text-xs">{{ p.value }}</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content>
<code>placement="{{ p.value }}"</code>
</twig:ui:Popover:Content>
</twig:ui:Popover>
</div>
{% endfor %}
</div>
Offset
<div class="flex gap-3">
<twig:ui:Popover>
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" {{ ...popover_trigger_attrs }}>Default</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-56">
6px gap, the default.
</twig:ui:Popover:Content>
</twig:ui:Popover>
<twig:ui:Popover :offset="24">
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" {{ ...popover_trigger_attrs }}>offset="24"</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-56">
24px gap.
</twig:ui:Popover:Content>
</twig:ui:Popover>
</div>
Preventing flip
Each bordered box below is position: relative, making it the popover's
positioning boundary instead of the viewport.
<div class="grid gap-6">
<p class="text-xs text-neutral-500 dark:text-neutral-400">
Each bordered box below is <code>position: relative</code>, making it the popover's
positioning boundary instead of the viewport.
</p>
<div class="grid gap-4">
<div class="relative flex h-32 items-end justify-center overflow-hidden rounded-md border border-neutral-200 dark:border-neutral-800">
<twig:ui:Popover :portalled="false">
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" size="sm" {{ ...popover_trigger_attrs }} class="my-2">Default</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-48">
Flips upward to stay in view.
</twig:ui:Popover:Content>
</twig:ui:Popover>
</div>
<div>
<div class="relative flex h-32 items-end justify-center rounded-md border border-neutral-200 dark:border-neutral-800">
<twig:ui:Popover :portalled="false" :shouldFlip="false">
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" size="sm" {{ ...popover_trigger_attrs }} class="my-2">shouldFlip="false"</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-48">
Stays below and overflows the box instead of flipping.
</twig:ui:Popover:Content>
</twig:ui:Popover>
</div>
<div class="relative z-[60] mt-2 flex h-24 items-center justify-center rounded-md border border-red-500 bg-red-500/5 text-center text-xs text-red-600 dark:text-red-400">
This space should be hidden
</div>
</div>
</div>
<div class="flex flex-wrap gap-3 justify-evenly w-full">
<twig:ui:Popover>
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" size="sm" {{ ...popover_trigger_attrs }}>Default (portalled)</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-48">
Flips if it would overflow the viewport.
</twig:ui:Popover:Content>
</twig:ui:Popover>
<twig:ui:Popover :shouldFlip="false">
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" size="sm" {{ ...popover_trigger_attrs }}>shouldFlip="false" (portalled)</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-48">
Never flips, even outside the viewport.
</twig:ui:Popover:Content>
</twig:ui:Popover>
</div>
</div>
Close button
Notifications
You have no unread notifications.
<twig:ui:Popover>
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" {{ ...popover_trigger_attrs }}>Open</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-72">
<twig:ui:Popover:Close />
<div class="grid gap-1">
<p class="text-sm leading-none font-medium text-neutral-950 dark:text-neutral-50">Notifications</p>
<p class="text-sm text-neutral-500 dark:text-neutral-400">You have no unread notifications.</p>
</div>
</twig:ui:Popover:Content>
</twig:ui:Popover>
Trigger indicator
<twig:ui:Popover>
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" {{ ...popover_trigger_attrs }} class="gap-2">
Sort by
<twig:ui:Popover:Indicator class="transition-transform duration-200 data-[state=open]:rotate-180">
<twig:ux:icon name="lucide:chevron-down" class="size-4" />
</twig:ui:Popover:Indicator>
</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-56">
<fieldset class="grid gap-2">
<label class="flex items-center gap-2">
<input type="radio" name="indicator-sort" checked /> Name (A to Z)
</label>
<label class="flex items-center gap-2">
<input type="radio" name="indicator-sort" /> Name (Z to A)
</label>
</fieldset>
</twig:ui:Popover:Content>
</twig:ui:Popover>
Modal
Rename file
Tab stays trapped inside until you close this.
<twig:ui:Popover :modal="true">
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" {{ ...popover_trigger_attrs }}>Open modal popover</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-72">
<div class="grid gap-1">
<p class="text-sm leading-none font-medium text-neutral-950 dark:text-neutral-50">Rename file</p>
<p class="text-sm text-neutral-500 dark:text-neutral-400">Tab stays trapped inside until you close this.</p>
</div>
<input
value="Project Proposal.pdf"
class="h-8 rounded-md border border-neutral-200 bg-transparent px-2 dark:border-neutral-700"
/>
<div class="flex justify-end gap-2">
<twig:ui:Button size="sm" data-scope="popover" data-part="close-trigger" data-owner="{{ inject('popover_id') }}">Save</twig:ui:Button>
</div>
</twig:ui:Popover:Content>
</twig:ui:Popover>
Matching the trigger's width
<div class="w-72">
<twig:ui:Popover :sameWidth="true">
<twig:ui:Popover:Trigger>
<twig:ui:Button
variant="outline"
{{ ...popover_trigger_attrs }}
class="w-full justify-start text-neutral-500 aria-expanded:scale-100 dark:text-neutral-400"
>
Add a comment…
</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content>
<textarea
rows="3"
placeholder="Write a comment"
class="w-full resize-none rounded-md border border-neutral-200 bg-transparent p-2 dark:border-neutral-700"
></textarea>
<div class="flex justify-end gap-2">
<twig:ui:Button size="sm">Comment</twig:ui:Button>
</div>
</twig:ui:Popover:Content>
</twig:ui:Popover>
</div>
Right-to-left
لا توجد إشعارات جديدة
ستظهر هنا الإشعارات الجديدة عند وصولها.
<twig:ui:Popover dir="rtl">
<twig:ui:Popover:Trigger>
<twig:ui:Button variant="outline" {{ ...popover_trigger_attrs }}>الإشعارات</twig:ui:Button>
</twig:ui:Popover:Trigger>
<twig:ui:Popover:Content class="w-72">
<twig:ui:Popover:Close />
<div class="grid gap-1">
<p class="text-sm leading-none font-medium text-neutral-950 dark:text-neutral-50">لا توجد إشعارات جديدة</p>
<p class="text-sm text-neutral-500 dark:text-neutral-400">ستظهر هنا الإشعارات الجديدة عند وصولها.</p>
</div>
</twig:ui:Popover:Content>
</twig:ui:Popover>
API Reference
<twig:ui:Popover>
| Prop | Type | Description |
|---|---|---|
open |
boolean |
Whether the popover is open on initial render. Defaults to false |
id |
string |
The popover's DOM id, used to scope its trigger(s) and content. Defaults to an auto-generated value |
modal |
boolean |
Whether the popover traps focus, blocks scroll, and hides the rest of the page from screen readers while open. Defaults to false |
portalled |
boolean |
Whether the content is moved to the end of <body> on connect. Defaults to true |
autoFocus |
boolean |
Whether to focus the first focusable element inside Popover:Content when it opens. Add autofocus (or data-autofocus) to a specific element to focus it instead. Defaults to true |
closeOnInteractOutside |
boolean |
Whether a pointer interaction outside Popover:Content closes the popover. Defaults to true |
closeOnEscape |
boolean |
Whether pressing escape closes the popover. Defaults to true |
placement |
Placement |
Where to place the content relative to its trigger, e.g. 'bottom', 'top-start'. Defaults to 'bottom' |
offset |
number |
The gap, in pixels, between the trigger and the content. Defaults to 6 |
shouldFlip |
boolean |
Whether to flip to the opposite side when the content would overflow the viewport. Defaults to true |
sameWidth |
boolean |
Whether to make the content the same width as the trigger. Defaults to false |
ariaLabel |
string |
Accessible name for Popover:Content when it doesn't wire up a data-part="title" element. Defaults to null |
dir |
'ltr' | 'rtl' |
Text direction applied to every popover part. Defaults to the page's <html dir> |
as |
string |
The element to render the root wrapper as. Defaults to div |
| Block | Description |
|---|---|
content |
The popover structure: one or more Popover:Trigger, an optional Popover:Anchor, and a Popover:Content |
<twig:ui:Popover:Trigger>
Exposes popover_trigger_attrs to spread onto the trigger element. Use several with different
values to share one popover across many triggers. aria-expanded also scales it down slightly
while open, the same press feedback Dialog triggers get.
| Prop | Type | Description |
|---|---|---|
value |
string |
Identifies this trigger among several inside the same Popover. Defaults to null |
| Block | Description |
|---|---|
content |
The trigger element (e.g. a Button with {{ ...popover_trigger_attrs }}) |
<twig:ui:Popover:Anchor>
Decouples the positioning reference from the trigger, e.g. opening from a toolbar button but pointing at selected text.
| Prop | Type | Description |
|---|---|---|
as |
string |
The element to render. Defaults to span |
| Block | Description |
|---|---|
content |
The reference element |
<twig:ui:Popover:Content>
Renders the positioner and content parts. There's no width prop; pass a literal w-* in class.
| Prop | Type | Description |
|---|---|---|
as |
string |
The element to render the content part as. Defaults to div |
| Block | Description |
|---|---|
content |
The popover content |
<twig:ui:Popover:Indicator>
Fades and recolors based on its own data-state="open" | "closed". Layer on your own
data-[state=open]:* classes (e.g. a rotate) for state-driven motion beyond the default fade.
| Prop | Type | Description |
|---|---|---|
as |
string |
The element to render. Defaults to span |
| Block | Description |
|---|---|
content |
The indicator's content, e.g. an icon. Empty by default |
<twig:ui:Popover:Close>
Exposes popover_close_attrs (data-scope="popover" data-part="close-trigger" data-owner="<popover id>") to spread onto a custom dismiss element. Defaults to a ghost icon
button in the top-right corner of Popover:Content.
| Block | Description |
|---|---|
content |
The dismiss element. Defaults to a Button with {{ ...popover_close_attrs }} |