Skip to content
cennge-ui
GitHubv0.3.1, changelog

HoverCard

A preview of where a link leads, shown while the pointer rests on it or the link has focus.

Recent activity

  1. Rest the pointer on a name: the card opens after half a second and stays while the pointer is on the link or the card.
  2. Tab to a link: focus opens the card too, and Escape closes it.
  3. On a touch screen a tap goes straight to the link's page.
Live: point at a name, or tab to it.

Field marks

Import
import { HoverCard, HoverCardTrigger, HoverCardContent } from "cennge-ui";
Size
0.7 KB gzip for this import, its dependencies aside
Pattern
Your own <a>, unchanged; the card is not announced
Keyboard
Tab Esc
State
Uncontrolled, or isOpen with onOpenChange
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Delays
500 ms to open, 200 ms to close
Touch
A tap follows the link at once
Tests
Unit tests; end-to-end in Chromium, Firefox and WebKit, on desktop and phone viewports: overlays

When to use#

Use a HoverCard to preview what is behind a link: the person behind a mention, the repository behind its name, the release behind its tag. People pointing at the link, or tabbing to it, get a look without leaving the page; on a touch screen the link works as usual.

Put nothing in the card that the linked page does not also have. On a touch screen a tap goes straight to that page, and keyboard users can see the card but not reach into it.

Similar#

  • Tooltip names a control in a few words; a HoverCard previews where a link leads, with room for a picture and a few lines.
  • Popover opens on a click and moves focus inside, so it can hold fields and buttons; a HoverCard is for looking, not for using.

A profile behind a mention#

HoverCardTrigger passes its events to the one element inside it, so the link keeps its href, its focus and its own styles. HoverCardContent renders in a portal, so a parent with hidden overflow cannot cut the card off.

Reviewed by @Cennge before the release.

mention.tsx
import { Avatar, HoverCard, HoverCardContent, HoverCardTrigger } from "cennge-ui";

export default function MentionCard() {
  return (
    <p className="text-sm">
      Reviewed by{" "}
      <HoverCard>
        <HoverCardTrigger>
          <a href="https://github.com/Cennge" className="font-semibold text-primary-text hover:underline">
            @Cennge
          </a>
        </HoverCardTrigger>
        <HoverCardContent>
          <div className="flex items-center gap-3">
            <Avatar name="Cennge" color="primary" />
            <div>
              <p className="font-semibold">Cennge</p>
              <p className="text-muted">Maintains cennge-ui and payload-cache</p>
            </div>
          </div>
        </HoverCardContent>
      </HoverCard>{" "}
      before the release.
    </p>
  );
}

At the foot of a page, placement="top" opens the card upwards and showArrow points it at its link. When there is no room on the chosen side, the card flips to the other one and keeps 8 pixels from the edge of the window.

placement.tsx
import { HoverCard, HoverCardContent, HoverCardTrigger } from "cennge-ui";

const repositories = [
  { name: "ai-niche-scout", description: "Validate an AI startup idea: competitors and 54 AI niches" },
  { name: "payload-cache", description: "FastAPI cache service" },
  { name: "spredo-crypto-screener", description: "Crypto screener" },
  { name: "playwright-msi-scraper", description: "Scraper on Playwright" },
];

export default function MoreRepositories() {
  return (
    <footer className="w-full border-t border-border pt-4 text-sm">
      <p className="text-muted">More from Cennge</p>
      <ul className="mt-2 flex flex-wrap gap-x-5 gap-y-2">
        {repositories.map((repository) => (
          <li key={repository.name}>
            <HoverCard>
              <HoverCardTrigger>
                <a href={`https://github.com/Cennge/${repository.name}`} className="font-semibold hover:underline">
                  {repository.name}
                </a>
              </HoverCardTrigger>
              <HoverCardContent placement="top" showArrow className="w-60">
                <p className="font-semibold">{repository.name}</p>
                <p className="mt-1 text-muted">{repository.description}</p>
              </HoverCardContent>
            </HoverCard>
          </li>
        ))}
      </ul>
    </footer>
  );
}

Delays#

openDelay waits 500 ms, so the card does not flash up while the pointer only crosses the link; closeDelay gives the pointer 200 ms to travel from the link into the card. In a row of small tags that people point at on purpose, shorter delays feel quicker.

delays.tsx
import { HoverCard, HoverCardContent, HoverCardTrigger } from "cennge-ui";

const releases = [
  { version: "0.3.1", summary: "The README's map examples are set in New York." },
  { version: "0.3.0", summary: "Charts in cennge-ui/charts, and theme switching without a flash." },
  { version: "0.2.0", summary: "Maps in cennge-ui/map, on MapLibre GL." },
  { version: "0.1.0", summary: "First release." },
];

export default function ReleaseTags() {
  return (
    <div className="flex flex-wrap items-center gap-2 text-sm">
      <span className="text-muted">Releases</span>
      {releases.map((release) => (
        <HoverCard key={release.version} openDelay={150} closeDelay={100}>
          <HoverCardTrigger>
            <a
              href="https://github.com/Cennge/cennge-ui/blob/main/CHANGELOG.md"
              className="rounded-full bg-surface-3 px-2.5 py-1 font-mono text-xs hover:bg-surface-4"
            >
              {release.version}
            </a>
          </HoverCardTrigger>
          <HoverCardContent className="w-64">
            <p className="font-semibold">cennge-ui {release.version}</p>
            <p className="mt-1 text-muted">{release.summary}</p>
          </HoverCardContent>
        </HoverCard>
      ))}
    </div>
  );
}

Accessibility#

The link is the accessible part. No ARIA attribute ties the card to it, so screen readers do not announce it, and links or buttons inside the card are taken out of the tab order. Keyboard users see the card when the link takes focus, after the same openDelay, and Escape closes it.

Styling#

HoverCardContent is 18rem wide and never wider than the window less 1rem, on surface-3 with a border and a 1rem padding. Its className is merged last, so w-60 or p-3 replace those values.

Props#

HoverCard#

PropTypeDefault
openDelay
numberDelay before opening, ms.
500
closeDelay
numberDelay before closing, ms.
200
isOpen
boolean
–
defaultOpen
boolean
–
onOpenChange
(isOpen: boolean) => void
–
children
React.ReactNode
–

HoverCardContent#

PropTypeDefault
placement
"left""right""top""bottom"
"bottom"
align
"start""center""end"
"center"
offset
number
8
showArrow
boolean
false
container
HTMLElement | null
–

Also takes every attribute of <div>.