HoverCard component — a rich floating panel revealed on hover with configurable delays.
A HoverCard is used to show a preview or supplementary information about a
linked element when the user hovers over it. It is more powerful than a
Tooltip because it can contain structured content — avatars, dates,
descriptions, action links — and it has independent open and close delays
so users can move into the panel without it disappearing.
Unlike a Popover, a HoverCard is triggered by hover (not click) and
carries role="tooltip" semantics — it is supplementary information, not
a required step in a workflow.
Typical Use Cases
- User profile previews on
@mentionlinks in chat or comments - Repository/project summaries on hover in a dashboard list
- Link preview cards (URL, title, description) in rich text
- Product card previews in an e-commerce catalogue
The PhiaHoverCard hook manages:
- Configurable open delay (prevents flash on quick cursor movement)
- Configurable close delay (allows the user to move cursor into the card)
- Smart viewport positioning and edge flipping
- Mouse and focus event handling
Sub-components
| Function | Element | Purpose |
|---|---|---|
hover_card/1 | div | Root container — hook mount point with delay attrs |
hover_card_trigger/1 | span | Hover area with aria-describedby |
hover_card_content/1 | div | Floating panel with role="tooltip" |
Hook Setup
Copy the hook via mix phia.add hover_card, then register it in app.js:
# assets/js/app.js
import PhiaHoverCard from "./hooks/hover_card"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { PhiaHoverCard }
})Basic Example — User Profile Preview
The canonical use case: hovering a username shows a brief profile card.
<.hover_card id="user-card-42">
<.hover_card_trigger hover_card_id="user-card-42">
<.link href="/users/janedoe" class="font-medium hover:underline">
@janedoe
</.link>
</.hover_card_trigger>
<.hover_card_content hover_card_id="user-card-42">
<div class="flex gap-3">
<.avatar src={@user.avatar_url} alt={@user.name} />
<div>
<p class="font-semibold">{@user.name}</p>
<p class="text-sm text-muted-foreground">@{@user.handle}</p>
<p class="text-sm text-muted-foreground mt-1">
Joined {Calendar.strftime(@user.joined_at, "%B %Y")}
</p>
</div>
</div>
<p class="mt-3 text-sm">{@user.bio}</p>
</.hover_card_content>
</.hover_card>Example — Repository Preview
Show project metadata on hover in a list view:
<.hover_card id="repo-card-phiaui" open_delay={400} close_delay={150}>
<.hover_card_trigger hover_card_id="repo-card-phiaui">
<.link href="/repos/phiaui">{@repo.name}</.link>
</.hover_card_trigger>
<.hover_card_content hover_card_id="repo-card-phiaui" side="right">
<p class="font-semibold">{@repo.name}</p>
<p class="text-sm text-muted-foreground">{@repo.description}</p>
<div class="flex gap-4 mt-2 text-xs text-muted-foreground">
<span>⭐ {@repo.stars}</span>
<span>🍴 {@repo.forks}</span>
<span>{@repo.language}</span>
</div>
</.hover_card_content>
</.hover_card>Example — Custom Positioning
Use side="top" when the trigger is near the bottom of the viewport:
<.hover_card id="bottom-card" side="top" open_delay={200} close_delay={100}>
<.hover_card_trigger hover_card_id="bottom-card">
<span class="cursor-help underline decoration-dotted">Learn more</span>
</.hover_card_trigger>
<.hover_card_content hover_card_id="bottom-card" side="top">
<p class="text-sm">Detailed explanation of this feature.</p>
</.hover_card_content>
</.hover_card>Side Values
| Value | Panel appears... |
|---|---|
"bottom" | Below the trigger (default) |
"top" | Above the trigger |
"left" | To the left of the trigger |
"right" | To the right of the trigger |
The hook may flip to the opposite side if the preferred side would overflow the viewport.
Delay Tuning
| Use Case | Recommended Delays |
|---|---|
| Dense lists, quick scanning | open_delay={500} close_delay={150} |
| Important previews (profile cards) | open_delay={300} close_delay={200} |
| Instant feedback (tight interaction) | open_delay={100} close_delay={100} |
Accessibility
hover_card_trigger/1setsaria-describedbypointing at the content panel, so screen readers announce the card content when focus lands on the trigger (keyboard users receive the same information as mouse users)hover_card_content/1usesrole="tooltip"— supplementary, non-interactive- If your card contains interactive elements (links, buttons), consider using
popover/1instead, which hasrole="dialog"and a proper focus trap
Summary
Functions
Renders the hover card root container and attaches the PhiaHoverCard hook.
Renders the floating hover card content panel.
Renders the hover trigger wrapper.
Functions
Renders the hover card root container and attaches the PhiaHoverCard hook.
The data-trigger="hover", data-open-delay, data-close-delay, and
data-side attributes pass configuration to the JS hook. The hook reads
these on mount to configure its event listeners and positioning logic.
Attributes
id(:string) (required) - Unique element ID — the hook mount point. Sub-components derive their IDs from this value. Use a unique value per card instance when rendering in a list (e.g."user-card-#{user.id}").open_delay(:integer) - Milliseconds to wait aftermouseenterbefore showing the card. A moderate delay (300–500ms) avoids flashing the card as the user's cursor passes through the trigger on the way to something else. Set lower for tighter interactions.Defaults to
300.close_delay(:integer) - Milliseconds to wait aftermouseleavebefore hiding the card. A close delay (150–300ms) gives the user time to move their cursor from the trigger into the floating card without it disappearing mid-movement.Defaults to
200.side(:string) - Preferred side for the floating panel. The hook may flip this if needed. Defaults to"bottom". Must be one of"top","bottom","left", or"right".class(:string) - Additional CSS classes for the container. Defaults tonil.Global attributes are accepted. Extra HTML attributes forwarded to the root
<div>.
Slots
inner_block(required) -hover_card_trigger/1andhover_card_content/1sub-components.
Renders the floating hover card content panel.
Hidden by default via display: none (the hidden class). The PhiaHoverCard
hook removes this class after the open_delay elapses and positions the
panel relative to the trigger using getBoundingClientRect.
Uses semantic Tailwind tokens (bg-popover, text-popover-foreground,
border-border) so the panel automatically respects dark mode and custom
theme presets without any additional CSS.
The data-hover-card-content attribute is the hook's selector for the
element to show/hide. The data-side attribute tells the hook which edge
to compute the initial offset from.
Attributes
hover_card_id(:string) (required) - ID of the parenthover_card/1— used to build the panel's elementid.side(:string) - Preferred display side for this content panel. Takes precedence over thesideset on the parenthover_card/1. Use this override when the container and content panel should logically have different preferred sides.Defaults to
"bottom". Must be one of"top","bottom","left", or"right".class(:string) - Additional CSS classes for the content panel. Defaults tonil.Global attributes are accepted. Extra HTML attributes forwarded to the panel
<div>.
Slots
inner_block(required) - Hover card body. Can contain structured content — avatars, descriptions, stats, inline links. Keep the layout compact and relevant. Note: if you need the user to interact with buttons or forms in the card, usepopover/1instead, which has a focus trap.
Renders the hover trigger wrapper.
Sets aria-describedby pointing at "{hover_card_id}-content". Screen
readers will announce the card content when the user focuses the trigger —
keyboard users receive the same information as mouse users.
The data-hover-card-trigger attribute is the hook's selector for attaching
mouse and focus event listeners.
Attributes
hover_card_id(:string) (required) - ID of the parenthover_card/1— used to buildaria-describedby.class(:string) - Additional CSS classes. Defaults tonil.- Global attributes are accepted. Extra HTML attributes forwarded to the wrapper
<span>.
Slots
inner_block(required) - The element that reveals the hover card. Typically an<a>link, a user handle, or any inline element. The hook attachesmouseenter/mouseleavelisteners todata-hover-card-trigger.