Skip to content

Navigation

Hover Card

A rich preview that opens on hover and keyboard focus, with open and close delays.

<script>
  import { HoverCard } from "@codeatlasdev/ui";
</script>

<HoverCard>
  {#snippet triggerChild(props)}<a href="/people/ada" {...props}>@ada</a>{/snippet}
  <strong>Ada Lovelace</strong>
  <p>Platform engineer.</p>
</HoverCard>

The card previews more about its trigger (a person, a link target, a record). The trigger stays the primary action: it is a real link or button that works without the card. The card opens when the pointer rests on the trigger for openDelay (700 ms) and when the trigger receives keyboard focus, at once. It stays open while the pointer crosses into it and closes closeDelay (300 ms) after the pointer or focus leaves, on Escape, and on blur. The trigger gets aria-describedby pointing at the card while it is open. Tab from the trigger moves into the card, so a link inside it is reachable from the keyboard; Escape inside the card closes it and returns focus to the trigger.

It is a hint layer: it never takes Escape or outside presses from a dialog or menu underneath. The card is a popover element in the browser top layer, positioned with CSS anchor positioning where supported.

Touch has no hover. A tap never opens the card; it only performs the trigger’s own action (follow the link, press the button), and the same information must be reachable from there. When the preview itself must be reachable on touch, use Popover (opens on tap) instead. Do not put content in a hover card that exists nowhere else, and do not use it for a text-only label: that is Tooltip.

Pass an interactive trigger (a link, Button, IconButton) through triggerChild and spread the received props on it; trigger wraps non-interactive content in its own button. bind:open controls it from code, and onOpenChange fires for user-driven changes only.

#940

Profile mention

A link in a sentence with a profile card. Hover it for the delay, or Tab to it for an instant card; Tab again to reach View profile; Escape closes.

Reviewed by @ada and merged this morning.

Reference #940 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { Avatar, HoverCard, Stack } from '@codeatlasdev/ui';
</script>

<p class="line">
  Reviewed by
  <HoverCard>
    {#snippet triggerChild(props)}<a class="mention" href="#ada" {...props}>@ada</a>{/snippet}
    <Stack gap="2">
      <strong class="name">Ada Lovelace</strong>
      <span class="meta">Platform engineer. Joined March 2024.</span>
      <a class="more" href="#ada-profile">View profile</a>
    </Stack>
  </HoverCard>
  and merged this morning.
</p>

<style>
  .line { margin: 0; font-size: var(--ca-font-size-body1); color: var(--ca-text-primary); }
  .mention { color: var(--ca-brand-text); font-weight: 600; text-decoration: none; border-radius: var(--ca-radius-xs); }
  .mention:hover { text-decoration: underline; }
  .name { font-size: var(--ca-font-size-body1); }
  .meta { color: var(--ca-text-secondary); }
  .more { color: var(--ca-brand-text); }
</style>
#941

Delays, side and bind:open

Short delays, placed above and aligned to the start. A second button shows the card from code, and onOpenChange reports only user-driven changes.

State: closed

Reference #941 · Pick in the gallery

Code
svelte
<script lang="ts">
  import { Button, HoverCard } from '@codeatlasdev/ui';

  let open = $state(false);
  let log = $state('closed');

  function onOpenChange(o: boolean) {
    log = o ? 'opened by the user' : 'closed by the user';
  }
</script>

<div class="row">
  <HoverCard bind:open openDelay={150} closeDelay={150} side="top" align="start" {onOpenChange}>
    {#snippet triggerChild(props)}<Button variant="outlined" {...props}>Plan details</Button>{/snippet}
    <p class="text">Team plan: 10 seats, 100 GB storage, priority support.</p>
  </HoverCard>
  <Button variant="ghost" onclick={() => (open = !open)}>{open ? 'Hide' : 'Show'} from code</Button>
</div>
<span class="note">State: {log}</span>

<style>
  .row { display: flex; gap: var(--ca-space-3); align-items: center; flex-wrap: wrap; }
  .text { margin: 0; color: var(--ca-text-secondary); }
  .note { font-size: var(--ca-font-size-body2); color: var(--ca-text-secondary); }
</style>

Props

PropTypeDefaultDescription
triggerChildSnippet<[Record<string, unknown>]>–Interactive trigger. Receives the trigger props to spread onto your link, Button or IconButton.
triggerSnippet–Non-interactive trigger content (text, icon), wrapped in the card's own button.
childrenSnippet–Card content.
openbooleanfalseBindable. Whether the card is open.
onOpenChange(open: boolean) => void–Called when the user opens or closes the card, not when the bound value is set from code.
openDelaynumber700Milliseconds the pointer must rest on the trigger before the card opens. Keyboard focus opens it at once.
closeDelaynumber300Milliseconds the card stays open after the pointer or focus leaves, so the pointer can cross into it.
side'top' | 'right' | 'bottom' | 'left''bottom'Preferred side of the trigger.
align'start' | 'center' | 'end''center'Alignment against the trigger.
sideOffsetnumber8Distance from the trigger in pixels.
classstring''Extra CSS classes on the card.