Skip to content

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

TooltipHoverCard
PurposeShort, non-interactive labelsRich preview content with interaction
ContentBrief textLinks, buttons, avatars, custom layouts
TriggerHover or focusHover or focus
ARIArole="tooltip"aria-haspopup="dialog"
FocusDoes not move focus into popupDoes not move focus into popup
Delay300ms default700ms 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.

PropTypeDefaultDescription
contentunknown-Content to display inside the hover-card popup.
childrenunknown-Trigger content when trigger is not provided.
triggerunknown-Custom trigger markup. Must include data-slot="hover-card-trigger".
triggerAs"button" | "span" | "div" | "a""span"Tag used for the generated trigger.
classstring-Additional CSS classes applied to the hover-card root.
classNamestring-Alias for class.
triggerClassstring-Additional CSS classes applied to the generated trigger.
triggerClassNamestring-Alias for triggerClass.
contentClassstring-Additional CSS classes applied to the hover-card popup.
contentClassNamestring-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.
sideOffsetnumber4Distance from the trigger in pixels.
alignOffsetnumber0Offset from the aligned edge in pixels.
avoidCollisionsbooleantrueFlip or shift to stay inside the viewport.
collisionPaddingnumber8Viewport edge padding used for collision handling.
delaynumber700Delay before showing the hover-card in milliseconds.
closeDelaynumber300Delay before closing after mouse leave in milliseconds.
skipDelayDurationnumber300Warm-up window where another hover-card opens without delay. Set 0 to disable.
portalbooleantruePortal content to document.body while open.
closeOnClickOutsidebooleantrueClose when clicking outside the hover-card.
closeOnEscapebooleantrueClose 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.

PropTypeDefaultDescription
as"button" | "span" | "div" | "a""span"Trigger tag name.
childrenunknown-Trigger content.
classstring-Additional CSS classes.
classNamestring-Alias for class.

HoverCard.Content

Popup container.

PropTypeDefaultDescription
childrenunknown-HoverCard content.
side"top" | "right" | "bottom" | "left""bottom"Preferred side styling.
classstring-Additional CSS classes.
classNamestring-Alias for class.

HoverCard.Title

Renders an h3 styled for hover-card headings.

PropTypeDefaultDescription
childrenunknown-Title content.
classstring-Additional CSS classes.
classNamestring-Alias for class.

HoverCard.Description

Renders a subdued paragraph.

PropTypeDefaultDescription
childrenunknown-Description content.
classstring-Additional CSS classes.
classNamestring-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.