HoverCard
A preview of where a link leads, shown while the pointer rests on it or the link has focus.
Recent activity
Cennge released cennge-ui 0.3.1
Nov 1Cennge pushed to payload-cache main
Oct 30Cennge opened an issue in spredo-crypto-screener
Oct 28
- 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.
- Tab to a link: focus opens the card too, and Escape closes it.
- On a touch screen a tap goes straight to the link's page.
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
isOpenwithonOpenChange - 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.
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>
);
}Above the link#
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.
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.
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#
HoverCardContent#
Also takes every attribute of <div>.