Skip to content
Areia
Esc
navigateopen⌘Jpreview
On this page

Hover Card

A popup that appears when a user hovers or focuses on an element, supporting interactive content.

Source Code

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.

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.

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.

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
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.

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.

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.

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.

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.

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 Escape hides 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-controls linking the trigger to the content.
  • aria-expanded on 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.

Was this page helpful?