HoverCard
HoverCard shows a preview card when the user hovers or focuses a trigger. Use it for profile and link previews. For content opened by a click, use Popover.
'use client';
import { Button } from '@/components/nxds/components/button/button';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/nxds/components/hover-card/hover-card';
export default function HoverCardDefault() {
return (
<HoverCard>
<HoverCardTrigger asChild>
<Button variant="link">@nxds</Button>
</HoverCardTrigger>
<HoverCardContent>
<div className="nx:flex nx:flex-col nx:gap-1">
<p className="nx:typography-label-default nx:font-semibold nx:text-foreground">
@nxds
</p>
<p className="nx:typography-body-default nx:text-muted-foreground">
The AI-native design system. Joined March 2026.
</p>
</div>
</HoverCardContent>
</HoverCard>
);
}
This example also needs: Button.
Installation
npx nxds-cli@latest add hover-card
Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.
Usage
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/nxds/components/hover-card/hover-card';
Composition
HoverCard
├── HoverCardTrigger
└── HoverCardContent
Examples
Profile card
Show an avatar, bio and join date in a wider preview card.
'use client';
import {
Avatar,
AvatarFallback,
AvatarImage,
} from '@/components/nxds/components/avatar/avatar';
import { Button } from '@/components/nxds/components/button/button';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/nxds/components/hover-card/hover-card';
export default function HoverCardProfileCard() {
return (
<HoverCard>
<HoverCardTrigger asChild>
<Button variant="link">@ada</Button>
</HoverCardTrigger>
<HoverCardContent className="nx:w-80">
<div className="nx:flex nx:gap-4">
<Avatar>
<AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace" />
<AvatarFallback>AL</AvatarFallback>
</Avatar>
<div className="nx:flex nx:flex-col nx:gap-1">
<p className="nx:typography-label-default nx:font-semibold nx:text-foreground">
Ada Lovelace
</p>
<p className="nx:typography-body-default nx:text-muted-foreground">
Wrote the first program for the Analytical Engine.
</p>
<p className="nx:typography-label-small nx:text-muted-foreground">
Joined December 1843
</p>
</div>
</div>
</HoverCardContent>
</HoverCard>
);
}
This example also needs: Avatar.
Placement
Set side on HoverCardContent to open the card on any edge of the trigger.
'use client';
import { Button } from '@/components/nxds/components/button/button';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/nxds/components/hover-card/hover-card';
const SIDES = [
{ side: 'top', label: 'Top' },
{ side: 'right', label: 'Right' },
{ side: 'bottom', label: 'Bottom' },
{ side: 'left', label: 'Left' },
] as const;
export default function HoverCardPlacement() {
return (
<div className="nx:flex nx:flex-wrap nx:gap-4">
{SIDES.map(({ side, label }) => (
<HoverCard key={side}>
<HoverCardTrigger asChild>
<Button variant="outline">{label}</Button>
</HoverCardTrigger>
<HoverCardContent side={side}>
Opens on the {side} of its trigger.
</HoverCardContent>
</HoverCard>
))}
</div>
);
}
Props
HoverCard
HoverCard adds no props of its own; it accepts the props of the element or primitive it wraps.
HoverCardContent
HoverCardContent adds no props of its own; it accepts the props of the element or primitive it wraps.
HoverCardTrigger
HoverCardTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.
HoverCard shows a preview card when the user hovers or focuses a trigger. Use it for profile and link previews. For content opened by a click, use Popover.
<script setup lang="ts">
import { Button } from '@/components/button';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/hover-card';
</script>
<template>
<HoverCard>
<HoverCardTrigger as-child>
<Button variant="link">@nxds</Button>
</HoverCardTrigger>
<HoverCardContent>
<div class="nx:flex nx:flex-col nx:gap-1">
<p
class="nx:typography-label-default nx:font-semibold nx:text-foreground"
>
@nxds
</p>
<p class="nx:typography-body-default nx:text-muted-foreground">
The AI-native design system. Joined March 2026.
</p>
</div>
</HoverCardContent>
</HoverCard>
</template>
This example also needs: Button.
Installation
- 1
Install the following dependencies:
npm install @vueuse/core@^14.1.0 class-variance-authority@^0.7.1 reka-ui@^2.11.0 - 2
Copy and paste the following code into your project.
components/hover-card/HoverCardContent.vue <script setup lang="ts"> import { reactiveOmit } from '@vueuse/core'; import { type FocusOutsideEvent, HoverCardContent, type HoverCardContentProps as RekaHoverCardContentProps, HoverCardPortal, type PointerDownOutsideEvent, useForwardPropsEmits, } from 'reka-ui'; import type { HTMLAttributes } from 'vue'; import { cn } from '../../lib/utils'; import { overlayFloatingTransitionClassName, popoverSurfaceClassName, } from '../overlay-layout/overlay-layout'; export interface HoverCardContentProps extends RekaHoverCardContentProps { class?: HTMLAttributes['class']; } defineOptions({ inheritAttrs: false }); const props = withDefaults(defineProps<HoverCardContentProps>(), { align: 'center', sideOffset: 4, }); // Reka emits these from HoverCardContent but exports no type for them. const emits = defineEmits<{ escapeKeyDown: [event: KeyboardEvent]; pointerDownOutside: [event: PointerDownOutsideEvent]; focusOutside: [event: FocusOutsideEvent]; interactOutside: [event: PointerDownOutsideEvent | FocusOutsideEvent]; }>(); const delegated = reactiveOmit(props, 'class'); const forwarded = useForwardPropsEmits(delegated, emits); </script> <template> <HoverCardPortal> <HoverCardContent v-bind="{ ...$attrs, ...forwarded }" data-slot="hover-card-content" :class=" cn( 'nx:z-popover nx:w-64 nx:p-container nx:outline-none', popoverSurfaceClassName, overlayFloatingTransitionClassName, props.class ) " > <slot /> </HoverCardContent> </HoverCardPortal> </template>components/hover-card/HoverCardTrigger.vue <script setup lang="ts"> import { HoverCardTrigger, type HoverCardTriggerProps as RekaHoverCardTriggerProps, } from 'reka-ui'; export type HoverCardTriggerProps = RekaHoverCardTriggerProps; const props = defineProps<HoverCardTriggerProps>(); </script> <template> <HoverCardTrigger v-bind="props" data-slot="hover-card-trigger"> <slot /> </HoverCardTrigger> </template>components/hover-card/index.ts export { default as HoverCardContent, type HoverCardContentProps, } from './HoverCardContent.vue'; export { default as HoverCardTrigger, type HoverCardTriggerProps, } from './HoverCardTrigger.vue'; export { HoverCardRoot as HoverCard, type HoverCardRootProps as HoverCardProps, } from 'reka-ui';components/overlay-layout/overlay-layout.ts import { cva, type VariantProps } from 'class-variance-authority'; /** * Popover-family surfaces share the translucent/blurred recipe; Tooltip uses a * solid carve-out because its small, brief text appears over arbitrary content. */ export const popoverSurfaceClassName = [ 'nx:rounded-md', 'nx:border-default', 'nx:border-border-default', 'nx:bg-popover-alpha', 'nx:backdrop-blur-md', 'nx:text-popover-foreground', 'nx:typography-body-default', 'nx:shadow-lg', ].join(' '); export const tooltipSurfaceClassName = [ 'nx:rounded-md', 'nx:border-default', 'nx:border-border-default', 'nx:bg-popover', 'nx:text-popover-foreground', 'nx:typography-body-default', 'nx:shadow-lg', ].join(' '); export const navigationMenuInlinePopoverSurfaceClassName = [ 'nx:group-data-[viewport=false]/navigation-menu:rounded-md', 'nx:group-data-[viewport=false]/navigation-menu:border-default', 'nx:group-data-[viewport=false]/navigation-menu:border-border-default', 'nx:group-data-[viewport=false]/navigation-menu:bg-popover-alpha', 'nx:group-data-[viewport=false]/navigation-menu:backdrop-blur-md', 'nx:group-data-[viewport=false]/navigation-menu:text-popover-foreground', 'nx:group-data-[viewport=false]/navigation-menu:shadow-lg', ].join(' '); export const navigationMenuInlinePopoverTransitionClassName = [ 'nx:group-data-[viewport=false]/navigation-menu:transition-[opacity,scale]', 'nx:group-data-[viewport=false]/navigation-menu:duration-fast', 'nx:group-data-[viewport=false]/navigation-menu:ease-move', 'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:scale-95', 'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:opacity-0', 'nx:group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-overlay-presence-exit', ].join(' '); // `animate-overlay-presence-exit` is a non-visual "presence bridge": Reka's // Presence only waits on `animationName`, not `transitionend`, so this inert // animation keeps a closing overlay mounted while the opacity/scale/translate // TRANSITIONS below run the visible exit. It animates an unread custom property (see // generateMotionUtilitiesCSS in @nxds/core), never a transitioned property, so it // cannot override those transitions. const overlayPresenceExitClassName = 'nx:data-[state=closed]:animate-overlay-presence-exit'; export const overlayFloatingTransitionClassName = [ 'nx:transition-[opacity,scale]', 'nx:duration-fast', 'nx:ease-move', 'nx:data-[state=closed]:scale-95', 'nx:data-[state=closed]:opacity-0', overlayPresenceExitClassName, ].join(' '); const overlayContentTransitionClassName = [ 'nx:transition-[opacity,scale]', 'nx:duration-default', 'nx:ease-move', 'nx:data-[state=closed]:scale-95', 'nx:data-[state=closed]:opacity-0', overlayPresenceExitClassName, ].join(' '); export const overlayScrimTransitionClassName = [ 'nx:transition-opacity', 'nx:duration-default', 'nx:ease-move', 'nx:data-[state=closed]:opacity-0', overlayPresenceExitClassName, ].join(' '); export const overlayPanelTransitionClassName = [ 'nx:transition-[translate]', 'nx:duration-slow', 'nx:ease-move', overlayPresenceExitClassName, ].join(' '); export const overlayContentVariants = cva( [ // Centred with margins, not a translate: a transform would make this the // containing block for fixed-position popups a dialog holds. 'nx:fixed nx:inset-0 nx:z-modal nx:m-auto nx:flex nx:h-fit nx:w-full nx:max-w-lg nx:flex-col', 'nx:max-h-[calc(100svh-2rem)] nx:overflow-hidden', 'nx:gap-4 nx:border-default nx:border-border-default nx:bg-container nx:py-6 nx:shadow-lg', overlayContentTransitionClassName, 'nx:sm:rounded-lg', ].join(' ') ); export const overlayScrimVariants = cva( [ 'nx:fixed nx:inset-0 nx:z-modal nx:bg-overlay', overlayScrimTransitionClassName, ].join(' ') ); export const overlayHeaderVariants = cva( 'nx:flex nx:shrink-0 nx:flex-col nx:gap-1 nx:px-6', { variants: { variant: { default: 'nx:text-center nx:sm:text-left', center: 'nx:items-center nx:text-center', }, }, defaultVariants: { variant: 'default', }, } ); export const overlayBodyClassName = [ 'nx:min-h-0', 'nx:overflow-y-auto', 'nx:px-6', 'nx:focus-visible:outline-2', 'nx:focus-visible:outline-focus-default', 'nx:focus-visible:[outline-offset:-2px]', ].join(' '); export const overlayFooterVariants = cva( 'nx:flex nx:shrink-0 nx:gap-2 nx:px-6', { variants: { orientation: { horizontal: 'nx:flex-col nx:sm:flex-row nx:sm:items-center nx:sm:justify-end', vertical: 'nx:flex-col nx:items-stretch nx:*:w-full', }, }, defaultVariants: { orientation: 'horizontal', }, } ); export const overlayCloseButtonClassName = [ 'nx:absolute nx:right-6 nx:top-6 nx:rounded-sm nx:border-0 nx:bg-transparent nx:p-1 nx:text-muted-foreground-subtle', 'nx:transition-control', 'nx:hover:bg-container-hover nx:hover:text-foreground', 'nx:focus-visible:bg-container-hover nx:focus-visible:text-foreground', 'nx:focus-visible:outline-2 nx:focus-visible:outline-focus-default', 'nx:disabled:pointer-events-none', ].join(' '); export type OverlayHeaderVariants = VariantProps<typeof overlayHeaderVariants>; export type OverlayVariant = NonNullable< VariantProps<typeof overlayHeaderVariants>['variant'] >; export type OverlayButtonOrientation = NonNullable< VariantProps<typeof overlayFooterVariants>['orientation'] >; export type OverlayLayoutContextValue = { variant: OverlayVariant; buttonOrientation: OverlayButtonOrientation; }; export const defaultOverlayLayout: OverlayLayoutContextValue = { variant: 'default', buttonOrientation: 'horizontal', }; export function resolveOverlayButtonOrientation( variant: OverlayVariant ): OverlayButtonOrientation { return variant === 'center' ? 'vertical' : 'horizontal'; } - 3
Keep each file at the path shown above it.
Paths are relative to
packages/vue/src/. Keep the same folders under your@/root, and the imports between files work without edits.
Usage
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/hover-card';
Examples
Profile card
Show an avatar, bio and join date in a wider preview card.
<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarImage } from '@/components/avatar';
import { Button } from '@/components/button';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/hover-card';
</script>
<template>
<HoverCard>
<HoverCardTrigger as-child>
<Button variant="link">@ada</Button>
</HoverCardTrigger>
<HoverCardContent class="nx:w-80">
<div class="nx:flex nx:gap-4">
<Avatar>
<AvatarImage src="/avatars/ada.svg" alt="Ada Lovelace" />
<AvatarFallback>AL</AvatarFallback>
</Avatar>
<div class="nx:flex nx:flex-col nx:gap-1">
<p
class="nx:typography-label-default nx:font-semibold nx:text-foreground"
>
Ada Lovelace
</p>
<p class="nx:typography-body-default nx:text-muted-foreground">
Wrote the first program for the Analytical Engine.
</p>
<p class="nx:typography-label-small nx:text-muted-foreground">
Joined December 1843
</p>
</div>
</div>
</HoverCardContent>
</HoverCard>
</template>
This example also needs: Avatar.
Placement
Set side on HoverCardContent to open the card on any edge of the trigger.
<script setup lang="ts">
import { Button } from '@/components/button';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/hover-card';
const sides = [
{ side: 'top', label: 'Top' },
{ side: 'right', label: 'Right' },
{ side: 'bottom', label: 'Bottom' },
{ side: 'left', label: 'Left' },
] as const;
</script>
<template>
<div class="nx:flex nx:flex-wrap nx:gap-4">
<HoverCard v-for="{ side, label } in sides" :key="side">
<HoverCardTrigger as-child>
<Button variant="outline">{{ label }}</Button>
</HoverCardTrigger>
<HoverCardContent :side="side"
>Opens on the {{ side }} of its trigger.</HoverCardContent
>
</HoverCard>
</div>
</template>
Props
HoverCardContent
HoverCardContent adds no props of its own; it accepts the props of the element or primitive it wraps.
HoverCardTrigger
HoverCardTrigger adds no props of its own; it accepts the props of the element or primitive it wraps.