Components Hover card

Petal Pro is the full SaaS app this is built for

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

Hover card

A rich preview on hover or focus - the profile card pattern. Twelve placements, open and close delays, pure intent handling.
The profile preview

Rest on the handle and the card fades in after 350ms. It stays up while you read it, and the button inside is a real button - the panel is a child of the wrapper, so hovering the card keeps it open. Tab to the link and it opens the same way, then tab again to reach Follow.

@jane
Jane Doe
@jane

Ships Phoenix apps for a living. Maintains three things she meant to archive.

1,204 followers 183 following
heex
<.hover_card>
<:trigger>
<a href="#" class="font-medium text-primary-600 dark:text-primary-400 hover:underline">
@jane
</a>
</:trigger>
<div class="w-64">
<div class="flex items-start gap-3">
<.avatar name="Jane Doe" size="md" />
<div class="min-w-0">
<div class="text-sm font-semibold">Jane Doe</div>
<div class="text-xs text-gray-500 dark:text-gray-400">@jane</div>
</div>
</div>
<p class="mt-3 text-sm text-gray-600 dark:text-gray-300">
Ships Phoenix apps for a living. Maintains three things she meant to archive.
</p>
<div class="flex items-center gap-4 mt-3 text-xs text-gray-500 dark:text-gray-400">
<span><span class="font-semibold text-gray-900 dark:text-white">1,204</span> followers</span>
<span><span class="font-semibold text-gray-900 dark:text-white">183</span> following</span>
</div>
<.button size="sm" color="primary" class="w-full mt-3" label="Follow" />
</div>
</.hover_card>

The same component on an inline link inside a sentence. A shorter open delay suits reading flow, since the pointer is already tracking the text. The trigger still goes to the page on click, on tap, and for anyone who never hovers at all.

Placement and timing

Twelve placements, the same geometry the popover panel uses. open_delay and close_delay are milliseconds; 0 opens on contact, which is worth trying before you decide the default is too slow.

opens instantly
Zero open delay and a 500ms close - forgiving of a wandering pointer.
waits 700ms
A 700ms open delay - deliberate rest only, nothing fires in passing.
heex
<%!-- justify-between and a narrower card below sm: these placements are
static, and on a phone a right-start card hung off a mid-row trigger
runs off the side of the pane --%>
<div class="flex w-full sm:w-auto items-center justify-between sm:justify-center gap-6 sm:gap-10 py-8">
<.hover_card placement="right-start" open_delay={0} close_delay={500}>
<:trigger>
<a href="#" class="text-sm font-medium underline">opens instantly</a>
</:trigger>
<div class="w-44 sm:w-56 text-sm">
Zero open delay and a 500ms close - forgiving of a wandering pointer.
</div>
</.hover_card>
<.hover_card placement="top-end" open_delay={700}>
<:trigger>
<a href="#" class="text-sm font-medium underline">waits 700ms</a>
</:trigger>
<div class="w-44 sm:w-56 text-sm">
A 700ms open delay - deliberate rest only, nothing fires in passing.
</div>
</.hover_card>
</div>
Properties
Attribute Type Default Description
card_class any nil extra classes for the card panel
class any nil extra classes for the wrapper
close_delay integer 150 milliseconds after the pointer leaves before the card closes
id string the panel id; autogenerated if not set
open_delay integer 350 milliseconds the pointer must rest on the trigger before the card opens
placement string "bottom" which side of the trigger the card appears on, with optional -start/-end alignment
one of: "top", "top-start", "top-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end", "right", "right-start", "right-end"
rest global
trigger_class any nil extra classes for the trigger element
:inner_block slot slot the card content
:trigger slot slot the trigger content (a link, chip, avatar, plain text)