Hover Card

A popup that appears when a link is hovered, showing a preview for sighted users. Built on Base UI's Preview Card.

"use client"

import { PreviewCard as PreviewCardPrimitive } from "@animate-ui/react/primitives/base-ui/preview-card"
import { cn } from "cn"
import { Button } from "@/components/ui/button"

function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
  return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />
}

function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) {
  return (
    <PreviewCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
  )
}

function HoverCardContent({
  className,
  side = "bottom",
  sideOffset = 4,
  align = "center",
  alignOffset = 4,
  ...props
}: PreviewCardPrimitive.Popup.Props &
  Pick<
    PreviewCardPrimitive.Positioner.Props,
    "align" | "alignOffset" | "side" | "sideOffset"
  >) {
  return (
    <PreviewCardPrimitive.Portal data-slot="hover-card-portal">
      <PreviewCardPrimitive.Positioner
        align={align}
        alignOffset={alignOffset}
        side={side}
        sideOffset={sideOffset}
        className="isolate z-50"
      >
        <PreviewCardPrimitive.Popup
          data-slot="hover-card-content"
          className={cn(
            "ring-foreground/10 bg-popover text-popover-foreground w-64 rounded-lg p-2.5 text-sm shadow-md ring-1 z-50 origin-(--transform-origin) outline-hidden",
            className
          )}
          {...props}
        />
      </PreviewCardPrimitive.Positioner>
    </PreviewCardPrimitive.Portal>
  )
}

export { HoverCard, HoverCardTrigger, HoverCardContent }

export default function HoverCardDemo({
  followCursor = false,
  side = "bottom",
  sideOffset = 4,
  align = "center",
  alignOffset = 4,
}: {
  followCursor?: boolean | "x" | "y"
  side?: "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end"
  sideOffset?: number
  align?: "start" | "center" | "end"
  alignOffset?: number
}) {
  return (
    <HoverCard followCursor={followCursor}>
      <HoverCardTrigger
        delay={10}
        closeDelay={100}
        render={<Button variant="link" />}
      >
        Hover Here
      </HoverCardTrigger>
      <HoverCardContent
        className="flex w-64 flex-col gap-0.5"
        side={side}
        sideOffset={sideOffset}
        align={align}
        alignOffset={alignOffset}
      >
        <div className="font-semibold">@nextjs</div>
        <div>The React Framework – created and maintained by @vercel.</div>
        <div className="mt-1 text-xs text-muted-foreground">
          Joined December 2021
        </div>
      </HoverCardContent>
    </HoverCard>
  )
}

Installation

shadcn/ui's Hover Card is built on Base UI's Preview Card.

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add hover-card
pnpm dlx shadcn@latest add hover-card
yarn dlx shadcn@latest add hover-card
bun x shadcn@latest add hover-card

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate hover-card
pnpm dlx @animate-ui/cli@latest animate hover-card
yarn dlx @animate-ui/cli@latest animate hover-card
bun x @animate-ui/cli@latest animate hover-card

Usage

Import the primitive from Animate UI instead of Base UI. Everything else stays the same.

import { PreviewCard } from '@animate-ui/react/primitives/base-ui/preview-card';

export function ProfileLink() {
  return (
    <PreviewCard.Root>
      <PreviewCard.Trigger href="https://github.com/imskyleen/animate-ui">
        @animate-ui
      </PreviewCard.Trigger>
      <PreviewCard.Portal>
        <PreviewCard.Positioner sideOffset={4}>
          <PreviewCard.Popup>
            Animated drop-in primitives for Radix UI, Base UI and React Aria.
          </PreviewCard.Popup>
        </PreviewCard.Positioner>
      </PreviewCard.Portal>
    </PreviewCard.Root>
  );
}

Using the @base-ui/react umbrella import instead? The barrel mirrors it: import { PreviewCard } from '@animate-ui/react/primitives/base-ui'.

Animation

The motion of Animate UI v1's Base UI Preview Card, value for value:

  • Popup grows from 0.8 and fades in, from the trigger (--transform-origin), on a spring (stiffness 400, damping 29) that overshoots a little before it settles.
  • Once the trigger's closeDelay is over, it closes the same way: it shrinks back to 0.8 on the same spring while it fades out, then unmounts.
  • Base UI only opens it on hover for mouse-like pointers: nothing sticks on touch screens.
  • With keepMounted on the Portal, the closed popup stays in the DOM: it plays its exit, then is hidden.
  • Your own render element (or function) on the Popup is made animatable rather than replaced.
  • Reopening while it closes picks up from where it is, on the same spring.
  • Open from the start (defaultOpen), it plays the same entrance on page load, like v1.
  • With followCursor on the Root, the popup leans towards the pointer while it moves over the trigger, by half its offset from the trigger's center, on a spring (stiffness 200, damping 17): on both axes, or only 'x' or 'y', as in v1. Under reduced motion it follows without the spring.
  • Under reduced motion, it only fades (no scale).

API Reference

Every part takes exactly the props of Base UI's Preview Card. Animate UI adds the animation props below.

Root

Base UI props, plus:

PropTypeDefault
followCursor?
boolean | 'x' | 'y'false
followCursorSpringOptions?
SpringOptions{ stiffness: 200, damping: 17 }

Trigger, Portal, Positioner, Arrow, Backdrop, Viewport

Base UI props only. They are not animated.

Base UI props, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 400, damping: 29 }
exitTransition?
Transitiontransition

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page