Hover Card
A popup that appears when a user hovers or focuses on an element, supporting interactive content.
Areia's HoverCard is built on @areia/slots/hover-card and rendered as an Ilha island when you use HoverCard. It handles hover/focus interactions, warm-up behavior between nearby hover-cards, collision-aware positioning, Escape dismissal, and accessible hover-card attributes.
Upcoming release
The next major version ships with improved performance and new primitives.
import ilha from "ilha";
import { CalendarDays } from "lucide";
import { Button, Icon, HoverCard } from "areia";
export default ilha.render(() => (
<HoverCard
content={
<div class="flex w-64 flex-col gap-2">
<HoverCard.Title>Upcoming release</HoverCard.Title>
<HoverCard.Description>
The next major version ships with improved performance
and new primitives.
</HoverCard.Description>
</div>
}
>
<Button
shape="square"
icon={<Icon icon={CalendarDays} />}
aria-label="Release notes"
/>
</HoverCard>
));Import
import { HoverCard } from "areia";
Usage
Call HoverCard(...) for an interactive island. The root renders the hover-card markup and initializes the @areia/slots/hover-card controller after mount.
Pro tip
Hover cards are great for previewing content without a click.
import ilha from "ilha";
import { Button, HoverCard } from "areia";
export default ilha.render(() => (
<HoverCard
content={
<div class="flex flex-col gap-1">
<HoverCard.Title>Pro tip</HoverCard.Title>
<HoverCard.Description>
Hover cards are great for previewing content without a
click.
</HoverCard.Description>
</div>
}
>
<Button>Hover me</Button>
</HoverCard>
));For static server-rendered markup or custom initialization, use HoverCard.Static(...). For most application usage, call HoverCard(...).
Examples
Basic HoverCard
Use content for the popup and children for the trigger content.
Did you know?
Hover cards support interactive content inside the popup.
import ilha from "ilha";
import { Info } from "lucide";
import { Button, Icon, HoverCard } from "areia";
export default ilha.render(() => (
<HoverCard
content={
<div class="flex flex-col gap-1">
<HoverCard.Title>Did you know?</HoverCard.Title>
<HoverCard.Description>
Hover cards support interactive content inside the
popup.
</HoverCard.Description>
</div>
}
>
<Button
variant="secondary"
shape="square"
icon={<Icon icon={Info} />}
aria-label="More information"
/>
</HoverCard>
));Text Trigger
When the trigger is not an action, render it as a span.
Areia
Areia
A vanilla TypeScript component library built for Astro and Ilha.
import ilha from "ilha";
import { HoverCard } from "areia";
export default ilha.render(() => (
<HoverCard
triggerAs="span"
triggerClass="underline decoration-dotted underline-offset-4"
content={
<div class="flex flex-col gap-1">
<HoverCard.Title>Areia</HoverCard.Title>
<HoverCard.Description>
A vanilla TypeScript component library built for Astro
and Ilha.
</HoverCard.Description>
</div>
}
>
Areia
</HoverCard>
));Multiple HoverCards
HoverCards share warm-up behavior automatically. After one hover-card closes, hovering another within skipDelayDuration opens it without the normal delay.
Calendar
View upcoming events.
Messages
Check your inbox.
Profile
Manage your account.
import ilha from "ilha";
import { CalendarDays, Mail, User } from "lucide";
import { Button, Icon, HoverCard } from "areia";
export default ilha.render(() => (
<div class="flex items-center gap-2">
<HoverCard
content={
<div class="flex flex-col gap-1">
<HoverCard.Title>Calendar</HoverCard.Title>
<HoverCard.Description>
View upcoming events.
</HoverCard.Description>
</div>
}
>
<Button
shape="square"
icon={<Icon icon={CalendarDays} />}
aria-label="Calendar"
/>
</HoverCard>
<HoverCard
content={
<div class="flex flex-col gap-1">
<HoverCard.Title>Messages</HoverCard.Title>
<HoverCard.Description>
Check your inbox.
</HoverCard.Description>
</div>
}
>
<Button
shape="square"
icon={<Icon icon={Mail} />}
aria-label="Messages"
/>
</HoverCard>
<HoverCard
content={
<div class="flex flex-col gap-1">
<HoverCard.Title>Profile</HoverCard.Title>
<HoverCard.Description>
Manage your account.
</HoverCard.Description>
</div>
}
>
<Button
shape="square"
icon={<Icon icon={User} />}
aria-label="Profile"
/>
</HoverCard>
</div>
));Side
Use side to choose the preferred side of the trigger. Collision handling may flip the hover-card at runtime when there is not enough space.
Top hover-card
Bottom hover-card
Left hover-card
Right hover-card
import ilha from "ilha";
import { Button, HoverCard } from "areia";
export default ilha.render(() => (
<div class="grid grid-cols-2 gap-3">
<HoverCard
side="top"
content={
<HoverCard.Description>
Top hover-card
</HoverCard.Description>
}
>
<Button>Top</Button>
</HoverCard>
<HoverCard
side="bottom"
content={
<HoverCard.Description>
Bottom hover-card
</HoverCard.Description>
}
>
<Button>Bottom</Button>
</HoverCard>
<HoverCard
side="left"
content={
<HoverCard.Description>
Left hover-card
</HoverCard.Description>
}
>
<Button>Left</Button>
</HoverCard>
<HoverCard
side="right"
content={
<HoverCard.Description>
Right hover-card
</HoverCard.Description>
}
>
<Button>Right</Button>
</HoverCard>
</div>
));Alignment
Use align to align the hover-card along the selected side.
Start aligned
Center aligned
End aligned
import ilha from "ilha";
import { Button, HoverCard } from "areia";
export default ilha.render(() => (
<div class="flex items-center gap-3">
<HoverCard
align="start"
content={
<HoverCard.Description>
Start aligned
</HoverCard.Description>
}
>
<Button>Start</Button>
</HoverCard>
<HoverCard
align="center"
content={
<HoverCard.Description>
Center aligned
</HoverCard.Description>
}
>
<Button>Center</Button>
</HoverCard>
<HoverCard
align="end"
content={
<HoverCard.Description>
End aligned
</HoverCard.Description>
}
>
<Button>End</Button>
</HoverCard>
</div>
));Delay Control
Use delay to control how long to wait before opening. Use closeDelay to control how long to wait before closing after leaving. Use skipDelayDuration to control the warm-up window after a hover-card closes.
Opens after one second
Opens and closes instantly with no warm-up window
import ilha from "ilha";
import { Button, HoverCard } from "areia";
export default ilha.render(() => (
<div class="flex flex-wrap items-center gap-3">
<HoverCard
delay={1000}
content={
<HoverCard.Description>
Opens after one second
</HoverCard.Description>
}
>
<Button>1s delay</Button>
</HoverCard>
<HoverCard
delay={0}
closeDelay={0}
skipDelayDuration={0}
content={
<HoverCard.Description>
Opens and closes instantly with no warm-up window
</HoverCard.Description>
}
>
<Button>Instant</Button>
</HoverCard>
</div>
));Custom Content
The content prop accepts markup. HoverCards are designed for richer content than tooltips.
Jane Doe
Product Designer
import ilha from "ilha";
import { Button, HoverCard } from "areia";
export default ilha.render(() => (
<HoverCard
content={
<div class="flex flex-col gap-3">
<div class="flex items-center gap-3">
<div class="flex size-9 items-center justify-center rounded-full bg-areia-surface-muted text-sm font-medium">
JD
</div>
<div>
<HoverCard.Title>Jane Doe</HoverCard.Title>
<HoverCard.Description>
Product Designer
</HoverCard.Description>
</div>
</div>
<div class="flex gap-2">
<Button size="sm">Follow</Button>
</div>
</div>
}
>
<Button>User preview</Button>
</HoverCard>
));Custom Trigger
Use trigger when you need complete control over the trigger markup. The custom trigger must include data-slot="hover-card-trigger".
import ilha from "ilha";
import { HoverCard } from "areia";
export default ilha.render(() => (
<HoverCard
content="Custom trigger markup"
trigger={
<span
data-slot="hover-card-trigger"
tabindex="0"
class="inline-flex rounded-full bg-areia-surface-muted px-2 py-1 text-sm text-areia-default"
>
Beta
</span>
}
/>
));Static Composition
Use low-level parts for static markup or when you want to call createHoverCard yourself.
Hover me
import ilha from "ilha";
import { HoverCard } from "areia";
export default ilha.render(() => (
<HoverCard.Static
content="Static hover-card markup"
triggerAs="span"
>
Hover me
</HoverCard.Static>
));HoverCard vs Tooltip
| Tooltip | HoverCard | |
|---|---|---|
| Purpose | Short, non-interactive labels | Rich preview content with interaction |
| Content | Brief text | Links, buttons, avatars, custom layouts |
| Trigger | Hover or focus | Hover or focus |
| ARIA | role="tooltip" | aria-haspopup="dialog" |
| Focus | Does not move focus into popup | Does not move focus into popup |
| Delay | 300ms default | 700ms default |
Use Tooltip to label an icon button or add a short explanation. Use HoverCard when you want to show richer preview content on hover.
API Reference
HoverCard
HoverCard is an Ilha island. It accepts standard HTML div attributes plus @areia/slots/hover-card options.
| Prop | Type | Default | Description |
|---|---|---|---|
content | unknown | - | Content to display inside the hover-card popup. |
children | unknown | - | Trigger content when trigger is not provided. |
trigger | unknown | - | Custom trigger markup. Must include data-slot="hover-card-trigger". |
triggerAs | "button" | "span" | "div" | "a" | "span" | Tag used for the generated trigger. |
class | string | - | Additional CSS classes applied to the hover-card root. |
className | string | - | Alias for class. |
triggerClass | string | - | Additional CSS classes applied to the generated trigger. |
triggerClassName | string | - | Alias for triggerClass. |
contentClass | string | - | Additional CSS classes applied to the hover-card popup. |
contentClassName | string | - | Alias for contentClass. |
side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side relative to the trigger. |
align | "start" | "center" | "end" | "center" | Preferred alignment along the selected side. |
sideOffset | number | 4 | Distance from the trigger in pixels. |
alignOffset | number | 0 | Offset from the aligned edge in pixels. |
avoidCollisions | boolean | true | Flip or shift to stay inside the viewport. |
collisionPadding | number | 8 | Viewport edge padding used for collision handling. |
delay | number | 700 | Delay before showing the hover-card in milliseconds. |
closeDelay | number | 300 | Delay before closing after mouse leave in milliseconds. |
skipDelayDuration | number | 300 | Warm-up window where another hover-card opens without delay. Set 0 to disable. |
portal | boolean | true | Portal content to document.body while open. |
closeOnClickOutside | boolean | true | Close when clicking outside the hover-card. |
closeOnEscape | boolean | true | Close when pressing Escape. |
onOpenChange | (open: boolean) => void | - | Called when visibility changes. |
onPortalMounted | (container: HTMLElement) => void | - | After portaled content mounts on open. See Dialog docs, Ilha + portaled overlays. |
HoverCard.Trigger
Generated trigger element.
| Prop | Type | Default | Description |
|---|---|---|---|
as | "button" | "span" | "div" | "a" | "span" | Trigger tag name. |
children | unknown | - | Trigger content. |
class | string | - | Additional CSS classes. |
className | string | - | Alias for class. |
HoverCard.Content
Popup container.
| Prop | Type | Default | Description |
|---|---|---|---|
children | unknown | - | HoverCard content. |
side | "top" | "right" | "bottom" | "left" | "bottom" | Preferred side styling. |
class | string | - | Additional CSS classes. |
className | string | - | Alias for class. |
HoverCard.Title
Renders an h3 styled for hover-card headings.
| Prop | Type | Default | Description |
|---|---|---|---|
children | unknown | - | Title content. |
class | string | - | Additional CSS classes. |
className | string | - | Alias for class. |
HoverCard.Description
Renders a subdued paragraph.
| Prop | Type | Default | Description |
|---|---|---|---|
children | unknown | - | Description content. |
class | string | - | Additional CSS classes. |
className | string | - | Alias for class. |
Accessibility
Behavior
- Opens on pointer hover for mouse/pen input after a delay.
- Opens on keyboard focus after a delay.
- Touch hover is ignored; touch devices use focus behavior.
- Pressing
Escapehides the hover-card. - Moving from the trigger to the hover-card content keeps it open.
- Disabled triggers do not open hover-cards.
- The popup can contain interactive content and remains open while the user interacts with it.
ARIA
The controller automatically handles:
aria-haspopup="dialog"on the trigger.aria-controlslinking the trigger to the content.aria-expandedon the trigger.- Stable generated IDs for content when needed.
Trigger Labels
HoverCards are supplemental. Icon-only triggers still need an accessible name, usually with aria-label on the trigger button.