A popover wraps a trigger slot (the clickable element) and a default slot (the floating panel).
Clicking the trigger toggles the panel; clicking outside or pressing Esc closes it.
Set the width and height of the selected layer in pixels.
<c-ui.popover>
<c-slot name="trigger"><c-ui.button>Open popover</c-ui.button></c-slot>
<div class="font-medium mb-1">Dimensions</div>
<p class="text-zinc-600 dark:text-zinc-400">Set the width and height of the selected layer in pixels.</p>
</c-ui.popover>
{% cotton ui.popover %}
{% cotton:slot trigger %}{% cotton ui.button %}Open popover{% endcotton %}{% endcotton:slot %}
<div class="font-medium mb-1">Dimensions</div>
<p class="text-zinc-600 dark:text-zinc-400">Set the width and height of the selected layer in pixels.</p>
{% endcotton %}
Use the position prop to place the panel on the top, bottom,
left or right of the trigger. The panel flips to the opposite side when there is
not enough room in the viewport. Adjust the gap with :offset.
Panel above the trigger.
Panel below the trigger.
Panel to the left.
Panel to the right.
<c-ui.popover position="top">
<c-slot name="trigger"><c-ui.button :outlined="True">Top</c-ui.button></c-slot>
<p>Panel above the trigger.</p>
</c-ui.popover>
<c-ui.popover position="bottom">
<c-slot name="trigger"><c-ui.button :outlined="True">Bottom</c-ui.button></c-slot>
<p>Panel below the trigger.</p>
</c-ui.popover>
<c-ui.popover position="left">
<c-slot name="trigger"><c-ui.button :outlined="True">Left</c-ui.button></c-slot>
<p>Panel to the left.</p>
</c-ui.popover>
<c-ui.popover position="right">
<c-slot name="trigger"><c-ui.button :outlined="True">Right</c-ui.button></c-slot>
<p>Panel to the right.</p>
</c-ui.popover>
{% cotton ui.popover position="top" %}
{% cotton:slot trigger %}{% cotton ui.button :outlined="True" %}Top{% endcotton %}{% endcotton:slot %}
<p>Panel above the trigger.</p>
{% endcotton %}
{% cotton ui.popover position="bottom" %}
{% cotton:slot trigger %}{% cotton ui.button :outlined="True" %}Bottom{% endcotton %}{% endcotton:slot %}
<p>Panel below the trigger.</p>
{% endcotton %}
{% cotton ui.popover position="left" %}
{% cotton:slot trigger %}{% cotton ui.button :outlined="True" %}Left{% endcotton %}{% endcotton:slot %}
<p>Panel to the left.</p>
{% endcotton %}
{% cotton ui.popover position="right" %}
{% cotton:slot trigger %}{% cotton ui.button :outlined="True" %}Right{% endcotton %}{% endcotton:slot %}
<p>Panel to the right.</p>
{% endcotton %}
Unlike a tooltip, a popover can hold any markup: headings, forms, inputs and buttons. Use the
class prop to widen the panel for richer layouts.
Update your display name and contact email.
<c-ui.popover class="max-w-sm w-80">
<c-slot name="trigger"><c-ui.button>Edit profile</c-ui.button></c-slot>
<div class="font-medium mb-1">Profile</div>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">Update your display name and contact email.</p>
<div class="space-y-3">
<c-ui.input label="Name" name="name" value="Ada Lovelace" />
<c-ui.input label="Email" name="email" type="email" value="ada@example.com" />
</div>
<div class="flex justify-end gap-2 mt-4">
<c-ui.button :outlined="True" x-on:click="close()">Cancel</c-ui.button>
<c-ui.button x-on:click="close()">Save</c-ui.button>
</div>
</c-ui.popover>
{% cotton ui.popover class="max-w-sm w-80" %}
{% cotton:slot trigger %}{% cotton ui.button %}Edit profile{% endcotton %}{% endcotton:slot %}
<div class="font-medium mb-1">Profile</div>
<p class="text-zinc-600 dark:text-zinc-400 mb-4">Update your display name and contact email.</p>
<div class="space-y-3">
{% cotton ui.input label="Name" name="name" value="Ada Lovelace" /%}
{% cotton ui.input label="Email" name="email" type="email" value="ada@example.com" /%}
</div>
<div class="flex justify-end gap-2 mt-4">
{% cotton ui.button :outlined="True" x-on:click="close()" %}Cancel{% endcotton %}
{% cotton ui.button x-on:click="close()" %}Save{% endcotton %}
</div>
{% endcotton %}
The panel exposes the popover's Alpine scope, so inner controls can call close() directly,
for example to dismiss the panel after a Save or Cancel action.
Set open_on="hover" to open the popover on hover instead of click, the "hovercard" pattern
seen on social profiles. A short intent delay avoids opening on a passing cursor, and a close delay lets you
move from the trigger into the panel without it snapping shut. Tune them with :open_delay and
:close_delay (milliseconds).
<c-ui.popover open_on="hover" position="top" :open_delay="120" :close_delay="200" class="w-64">
<c-slot name="trigger"><a href="#" @click.prevent class="text-accent font-medium no-underline cursor-pointer">@ada</a></c-slot>
<div class="flex items-center gap-3">
<c-ui.avatar initials="AL" color="violet" />
<div>
<div class="font-medium text-zinc-900 dark:text-zinc-100">Ada Lovelace</div>
<div class="text-zinc-500 dark:text-zinc-400">Mathematician</div>
</div>
</div>
</c-ui.popover>
{% cotton ui.popover open_on="hover" position="top" :open_delay="120" :close_delay="200" class="w-64" %}
{% cotton:slot trigger %}<a href="#" @click.prevent class="text-accent font-medium no-underline cursor-pointer">@ada</a>{% endcotton:slot %}
<div class="flex items-center gap-3">
{% cotton ui.avatar initials="AL" color="violet" /%}
<div>
<div class="font-medium text-zinc-900 dark:text-zinc-100">Ada Lovelace</div>
<div class="text-zinc-500 dark:text-zinc-400">Mathematician</div>
</div>
</div>
{% endcotton %}
The popover dispatches a popover-open event (and popover-close) when it opens, so you
can load the panel's contents only when needed, for example fetching a profile card the first time it is
shown. Listen for the event on the tag or any ancestor, where your own Alpine scope is in reach. The example
below fakes a fetch each time it opens, showing a spinner while it loads.
Fetched when the popover opened.
<div
x-data="{ loaded: false, loading: false, load() { this.loaded = false; this.loading = true; fetch('/api/profile/1').then(r => r.json()).then(d => { this.data = d; this.loaded = true; this.loading = false; }); } }"
@popover-open="load()"
>
<c-ui.popover open_on="hover" position="top" class="w-64">
<c-slot name="trigger"><c-ui.button :outlined="True">Hover to load</c-ui.button></c-slot>
<template x-if="loading">
<div class="flex items-center gap-2 text-zinc-500"><c-ui.spinner size="sm" color="current" /> Loading...</div>
</template>
<template x-if="loaded">
<div x-text="data.name"></div>
</template>
</c-ui.popover>
</div>
<div
x-data="{ loaded: false, loading: false, load() { this.loaded = false; this.loading = true; fetch('/api/profile/1').then(r => r.json()).then(d => { this.data = d; this.loaded = true; this.loading = false; }); } }"
@popover-open="load()"
>
{% cotton ui.popover open_on="hover" position="top" class="w-64" %}
{% cotton:slot trigger %}{% cotton ui.button :outlined="True" %}Hover to load{% endcotton %}{% endcotton:slot %}
<template x-if="loading">
<div class="flex items-center gap-2 text-zinc-500">{% cotton ui.spinner size="sm" color="current" /%} Loading...</div>
</template>
<template x-if="loaded">
<div x-text="data.name"></div>
</template>
{% endcotton %}
</div>
For an HTMX panel, load on the same event instead: put
hx-get="/profile/1" hx-trigger="popover-open once from:closest [data-popover]" on the content,
and it fetches when the popover first opens.
| Name | Description | Type | Options | Default |
|---|---|---|---|---|
position |
Side of the trigger the panel opens on. Flips to the opposite side when there is no room. | str | top, bottom, left, right | bottom |
open_on |
How the panel opens. Hover adds intent delays and is the hovercard pattern. | str | click, hover | click |
offset |
Gap in pixels between the trigger and the panel. | int | Any number | 8 |
open_delay |
Hover mode only. Milliseconds the cursor must rest on the trigger before opening. | int | Any number | 120 |
close_delay |
Hover mode only. Milliseconds before closing after the cursor leaves, so you can move into the panel. | int | Any number | 200 |
class |
Extra classes appended to the panel, for example to set its width. | str | Any Tailwind classes |
| Name | Description | Type | Options | Default |
|---|---|---|---|---|
popover-open |
Dispatched when the panel opens. Listen with @popover-open on the tag or an ancestor to lazy-load content. | event | — | — |
popover-close |
Dispatched when the panel closes. | event | — | — |
| Name | Description | Type | Options | Default |
|---|---|---|---|---|
trigger |
The clickable element that toggles the panel. | — | — | Optional |
slot (default) |
The floating panel content. Any rich markup, including forms and buttons. | — | — | Required |
Escape and outside-click close the panel. The trigger sets aria-expanded and the panel uses role="dialog".