Hover Card
A preview of what's behind a link, shown to pointer and keyboard users while they hover or focus it.
- v1.0.0
- WAI-ARIA: Tooltip
- SSR
- Zoneless
- 3.0 kB gzip
- +11.3 kB Angular CDK, shared
import { Component } from '@angular/core';
import { Avatar, AvatarFallback } from '@/ui/avatar/avatar';
import { Button } from '@/ui/button/button';
import {
HoverCardContent,
HoverCard,
HoverCardPortal,
HoverCardTrigger,
} from '@/ui/hover-card/hover-card';
/**
* A profile preview behind a username link. The link says where it goes
* on its own; the card adds a glance for sighted users.
*/
@Component({
selector: 'example-hover-card-demo',
imports: [
Avatar,
AvatarFallback,
Button,
HoverCardContent,
HoverCard,
HoverCardPortal,
HoverCardTrigger,
],
template: `
<ng-container ndHoverCard>
<a ndButton variant="link" ndHoverCardTrigger href="#angular">
@angular
</a>
<ng-template ndHoverCardPortal>
<div ndHoverCardContent class="w-80">
<div class="flex justify-between gap-4">
<span ndAvatar><span ndAvatarFallback>NG</span></span>
<div class="space-y-1">
<p class="text-sm font-semibold">@angular</p>
<p class="text-sm">
The web development framework for building modern apps.
</p>
<p class="text-xs text-muted-foreground">Joined September 2016</p>
</div>
</div>
</div>
</ng-template>
</ng-container>
`,
})
export class HoverCardDemoExample {}
A hover card previews what's behind a link, such as a person's profile behind their username, for sighted users: it opens after a short delay while the link is hovered or focused with the keyboard, and stays open while the pointer is over it. Screen readers don't announce it, so the link must make sense without it.
Installation
npx @ndop/cli add hover-cardpnpm dlx @ndop/cli add hover-cardyarn dlx @ndop/cli add hover-cardbunx @ndop/cli add hover-cardInstall the dependencies
npm install @ndop/primitives @ndop/corepnpm add @ndop/primitives @ndop/coreyarn add @ndop/primitives @ndop/corebun add @ndop/primitives @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/hover-card/hover-card.tsimport { computed, Directive, input } from '@angular/core'; import { NdTooltip } from '@ndop/primitives/tooltip'; import { NdHoverCardContent, NdHoverCardPortal, NdHoverCardTrigger, } from '@ndop/primitives/hover-card'; import { cn } from '@/lib/utils'; /** * A hover card (`@ndop/primitives/hover-card`): a preview of what's behind * a link, for sighted users, after a short delay on hover or keyboard * focus. Put it on an `ng-container`. * * @example * ```html * <ng-container ndHoverCard>…</ng-container> * ``` */ @Directive({ selector: '[ndHoverCard]', hostDirectives: [ { directive: NdTooltip, inputs: ['open', 'defaultOpen', 'openDelay', 'closeDelay'], outputs: ['openChange'], }, ], }) export class HoverCard {} /** * The link its card previews. * * @example * ```html * <a ndButton variant="link" ndHoverCardTrigger href="/ada">@ada</a> * ``` */ @Directive({ selector: '[ndHoverCardTrigger]', hostDirectives: [NdHoverCardTrigger], host: { 'data-slot': 'hover-card-trigger' }, }) export class HoverCardTrigger {} /** * Marks the `ng-template` its card renders while open. * * @example * ```html * <ng-template ndHoverCardPortal>…</ng-template> * ``` */ @Directive({ selector: 'ng-template[ndHoverCardPortal]', hostDirectives: [NdHoverCardPortal], }) export class HoverCardPortal {} /** * A hover card (`@ndop/primitives/hover-card`): a preview of what's behind * a link, shown after a short delay while the link is hovered or focused * with the keyboard, and kept open while the pointer is over it. It's for * sighted users only, so the link must make sense without it. * * @example * ```html * <ng-container ndHoverCard> * <a ndButton variant="link" ndHoverCardTrigger href="/nextjs">@nextjs</a> * <ng-template ndHoverCardPortal> * <div ndHoverCardContent>…</div> * </ng-template> * </ng-container> * ``` */ @Directive({ selector: '[ndHoverCardContent]', hostDirectives: [ { directive: NdHoverCardContent, inputs: ['side', 'align', 'sideOffset'], }, ], host: { 'data-slot': 'hover-card-content', '[class]': 'classes()' }, }) export class HoverCardContent { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=start]:slide-in-from-end-2 data-[side=end]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95', this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import { Avatar, AvatarFallback } from '@/ui/avatar/avatar';
import { Button } from '@/ui/button/button';
import {
HoverCardContent,
HoverCard,
HoverCardPortal,
HoverCardTrigger,
} from '@/ui/hover-card/hover-card';<ng-container ndHoverCard>
<a ndButton variant="link" ndHoverCardTrigger href="#angular">
@angular
</a>
<ng-template ndHoverCardPortal>
<div ndHoverCardContent class="w-80">
<div class="flex justify-between gap-4">
<span ndAvatar><span ndAvatarFallback>NG</span></span>
<div class="space-y-1">
<p class="text-sm font-semibold">@angular</p>
<p class="text-sm">
The web development framework for building modern apps.
</p>
<p class="text-xs text-muted-foreground">Joined September 2016</p>
</div>
</div>
</div>
</ng-template>
</ng-container>Examples
Hover Card sides
Cards on each side of their links.
import { Component } from '@angular/core';
import { Button } from '@/ui/button/button';
import {
HoverCardContent,
HoverCard,
HoverCardPortal,
HoverCardTrigger,
} from '@/ui/hover-card/hover-card';
/**
* Cards on each side of their links; they flip when there's no room.
* `start` and `end` swap in right-to-left layouts.
*/
@Component({
selector: 'example-hover-card-sides',
imports: [
Button,
HoverCardContent,
HoverCard,
HoverCardPortal,
HoverCardTrigger,
],
template: `
<div class="flex flex-wrap gap-2">
@for (side of sides; track side) {
<ng-container ndHoverCard>
<a
ndButton
variant="outline"
ndHoverCardTrigger
class="capitalize"
[href]="'#' + side"
>
{{ side }} side
</a>
<ng-template ndHoverCardPortal>
<div ndHoverCardContent [side]="side">
<p class="text-sm">This card opens on the {{ side }} side.</p>
</div>
</ng-template>
</ng-container>
}
</div>
`,
})
export class HoverCardSidesExample {
protected readonly sides = ['top', 'end', 'bottom', 'start'] as const;
}
Accessibility
Implements the Tooltip pattern of the WAI-ARIA Authoring Practices.
Keyboard
Keys the component handles.
| Key | Action |
|---|---|
| Tab | Focusing the trigger shows the tooltip; moving focus away hides it. |
| Escape | Hides the tooltip without moving focus. |
| Tab | Focusing the trigger opens the card after the delay; leaving it closes it. |
| Escape | Closes the card without moving focus. |
Focus
- The tooltip never takes focus; focus stays on the trigger.
- The card never takes focus; focus stays on the trigger.
Right-to-left
As in Popover's spec.md: start and end sides and alignments swap in RTL. start and end sides are logical and swap in right-to-left layouts.
API reference
HoverCard
[ndHoverCard]A hover card (@ndop/primitives/hover-card): a preview of what's behind
a link, for sighted users, after a short delay on hover or keyboard
focus. Put it on an ng-container.
| HoverCard binding | Type | Default |
|---|---|---|
[defaultOpen]Initial open state when open is not bound. | boolean | false |
[openDelay]Delay before opening on hover, in milliseconds. | number | 700 |
[closeDelay]Delay before closing once the pointer has left, in milliseconds. | number | 300 |
[(open)]Whether the tooltip is open. Two-way bindable with [(open)]. | boolean | false |
HoverCardTrigger
[ndHoverCardTrigger]The link its card previews.
| Data attribute | Values |
|---|---|
data-slot | hover-card-trigger |
data-state | — |
HoverCardPortal
ng-template[ndHoverCardPortal]Marks the ng-template its card renders while open.
HoverCardContent
[ndHoverCardContent]A hover card (@ndop/primitives/hover-card): a preview of what's behind
a link, shown after a short delay while the link is hovered or focused
with the keyboard, and kept open while the pointer is over it. It's for
sighted users only, so the link must make sense without it.
| HoverCardContent binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
[side]Side of the trigger it sits on, before flipping. | 'bottom' | 'top' | 'start' | 'end' | 'bottom' |
[align]Alignment along that side. | 'start' | 'end' | 'center' | 'center' |
[sideOffset]Gap between the trigger and the card, in pixels. | number | 4 |
| Data attribute | Values |
|---|---|
data-slot | hover-card-content |
data-state | — |
data-side | — |
data-align | — |
Agent notes
Do
- Use it for a preview of something people can also open.
- Keep it short: a name, a line of text, a date.
Don't
- Don't put anything only in the card: what it says must be on the page it previews.
- Don't put buttons or forms in it; use a Popover.