HoverCard
A preview of what a link points to, shown while the pointer rests on it.
Basic
Rest the pointer on the link, or Tab to it. The card opens after 700ms and stays open while the pointer moves into it.
Built with
and a lot of coffee.Code
<HoverCard>
<a slot="trigger" href="#">@elements</a>
<div class="hover-card-media">
<span class="avatar">EL</span>
<div>
<strong>@elements</strong>
<p>One environment for building and deploying web apps.</p>
<p class="meta">Joined October 2026</p>
</div>
</div>
</HoverCard>Usage
import { HoverCard, HoverCardTrigger } from "@elements/ui/hover-card";
<HoverCard>
<a slot="trigger" href="/u/ada">@ada</a>
<div class="hover-card-media">
<img src={user.avatar} alt="" width="40" height="40">
<div>
<strong>Ada Lovelace</strong>
<p>Wrote the first program.</p>
</div>
</div>
</HoverCard>Examples
Sides
side picks where the card opens; it flips when there is no room. arrow points at the trigger.
Code
<HoverCard side="top" size="auto" arrow>
<Button slot="trigger" variant="ghost">Top</Button>
Opens above.
</HoverCard>RTL
inline-start and inline-end follow the text direction: in a right-to-left region inline-start opens on the right and inline-end on the left.
Code
<div dir="rtl">
<HoverCard side="inline-start" size="auto" arrow>
<Button slot="trigger" variant="ghost">بداية السطر</Button>
يفتح على اليمين.
</HoverCard>
<HoverCard side="inline-end" size="auto" arrow>...</HoverCard>
</div>No delay
openDelay={0} and closeDelay={0}: opens on the first hover, closes the moment the pointer leaves trigger and card. Moving to the neighbouring trigger swaps the cards.
Code
<HoverCard openDelay={0} closeDelay={0} size="sm">
<a slot="trigger" href="#">instant</a>
Opens on the first hover and closes as soon as the pointer leaves.
</HoverCard>
<HoverCard openDelay={0} closeDelay={0} size="sm">
<a slot="trigger" href="#">neighbour</a>
Moving from one trigger to the next swaps the cards.
</HoverCard>Controlled
Bind open to state. onOpenChange reports the card's own changes, such as Escape or the pointer leaving.
Code
<Button onclick={() => state.preview = !state.preview}>Toggle</Button>
<HoverCard open={state.preview} onOpenChange={(open) => log(open)}>
<a slot="trigger" href="#">Ada Lovelace</a>
...
</HoverCard>Align and anchor
align lines the card up with the trigger's start, center or end. anchor positions it against another element on the page.
Code
<HoverCard align="start">
<a slot="trigger" href="/u/ada">@ada</a>
...
</HoverCard>
<HoverCard anchor="#profile-row">...</HoverCard>Hide when detached
hideWhenDetached hides the card while its trigger is scrolled out of view.
Tab to the link, then scroll this box.
Code
<div class="scroller">
<HoverCard hideWhenDetached>
<a slot="trigger" href="/u/ada">@ada</a>
...
</HoverCard>
</div>Events
onOpenChangeComplete fires after each animation; onEscapeKeyDown and onPointerDownOutside fire before the card closes, and preventDefault keeps it open.
Code
<HoverCard onOpenChangeComplete={(open) => log(open)}
onEscapeKeyDown={(e) => count(e)}
onPointerDownOutside={(e) => count(e)}>...</HoverCard>
<Button onclick={() => state.pinned = !state.pinned}>Pin card</Button>
<HoverCard open={state.pinned}
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}>...</HoverCard>Offsets and collisions
sideOffset and alignOffset move the card off its trigger. collisionPadding sets the room kept from the viewport edge; avoidCollisions={false} keeps it on its side with no flip or shift.
Code
<HoverCard align="start" sideOffset={12} alignOffset={8}>...</HoverCard>
<HoverCard align="end" collisionPadding={24} size="lg">...</HoverCard>
<HoverCard side="left" avoidCollisions={false}>...</HoverCard>Collision avoidance and position method
collisionAvoidance tunes how it stays in view: side "none" keeps the side with no flip while align "shift" still slides it into view; side "shift" keeps the side and slides it over the trigger into view; fallbackAxisSide "start" tries the inline start side before the end. positionMethod="absolute" places it in page coordinates.
Code
<HoverCard side="top" collisionAvoidance={{ side: "none", align: "shift" }}>...</HoverCard>
<HoverCard side="top" collisionAvoidance={{ side: "shift" }}>...</HoverCard>
<HoverCard side="top" collisionAvoidance={{ fallbackAxisSide: "start" }}>...</HoverCard>
<HoverCard positionMethod="absolute">...</HoverCard>Function offsets
sideOffset and alignOffset also take a function of the anchor's and the card's sizes, the side and the alignment.
Code
<HoverCard sideOffset={(d) => d.anchor.height}>
<a slot="trigger" href="/u/ada">@ada</a>
...
</HoverCard>Collision boundary, sticky and arrow padding
collisionBoundary keeps the card inside an element as well as the viewport. sticky="always" keeps it in the boundary after its trigger leaves. arrowPadding keeps the arrow off the corners.
Code
<div id="panel">
<HoverCard align="start" collisionBoundary="#panel" size="sm">...</HoverCard>
</div>
<HoverCard side="right" sticky="always" collisionBoundary="#list">...</HoverCard>
<HoverCard arrow align="start" arrowPadding={80}>...</HoverCard>Change reasons
onOpenChange gets a second argument: the reason for the change, the event, and cancel() to keep the current state.
Code
<!-- details.trigger is the trigger element that caused the change, if one did -->
<HoverCard onOpenChange={(open, details) => log(open, details.reason, details.trigger)}>
<a slot="trigger" href="/u/ada">@ada</a>
...
</HoverCard>
<!-- details.cancel() keeps the current state -->
<HoverCard onOpenChange={(open, details) => details.reason === "escape-key" && details.cancel()}>...</HoverCard>Sizes
size sets the width: auto fits the content, sm, md (the default) and lg are fixed steps.
Code
<HoverCard size="auto">...</HoverCard>
<HoverCard size="sm">...</HoverCard>
<HoverCard>...</HoverCard> <!-- md -->
<HoverCard size="lg">...</HoverCard>Constrain the content size
A tall card stays inside the viewport and scrolls; with an arrow the arrow stays visible outside the scroll box.
Code
<HoverCard arrow size="sm">
<a slot="trigger" href="/changelog">changelog</a>
<ul>...twenty releases...</ul>
</HoverCard>Multiple triggers
HoverCardTrigger with a handle opens one card from several links. It sits against the link the pointer rests on and moves to the next one; payload carries that link's data, and PopoverViewport cross-fades the content.
Code
<HoverCardTrigger handle="person" payload="Ada Lovelace"><a href="/u/ada">@ada</a></HoverCardTrigger>
<HoverCardTrigger handle="person" payload="Grace Hopper"><a href="/u/grace">@grace</a></HoverCardTrigger>
<!-- open, triggerId and payload are two-way: set triggerId and open to show it on a chosen link -->
<HoverCard handle="person" open={state.open} triggerId={state.link} payload={state.person} size="sm">
<PopoverViewport>{state.person}</PopoverViewport>
</HoverCard>Backdrop
backdrop dims the page under the open card. The pointer passes through it, so the link and the card keep working.
Read the
first.Code
<HoverCard backdrop>
<a slot="trigger" href="/docs">docs</a>
...
</HoverCard>Keep mounted
keepMounted keeps the closed card in the page, hidden, so what it holds stays as it was.
Ask
anything.Code
<HoverCard keepMounted>
<a slot="trigger" href="/support">support</a>
<input aria-label="Note"> <!-- keeps what was typed while closed -->
</HoverCard>Disable anchor tracking
disableAnchorTracking places the card once when it opens; it stays where it is while the page scrolls.
See
for plans.Code
<HoverCard disableAnchorTracking>
<a slot="trigger" href="/pricing">pricing</a>
...
</HoverCard>Open on load
open={true} without a binding: the card is open when the page loads, then owns its state.
Code
<HoverCard open={true}>
<a slot="trigger" href="/u/ada">@ada</a>
Open when the page loads. The card owns open from here.
</HoverCard>