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

<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

{% 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.

This space should be hidden
<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

<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>
<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&hellip;
            </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 }}