Hover Card

For sighted users to preview content available behind a link.

"use client"

import * as React from "react"
import { cn } from "cn"
import { HoverCard as HoverCardPrimitive } from "@animate-ui/react/primitives/radix-ui"
import { Button } from "@/components/ui/button"

function HoverCard({
  ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
  return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
}

function HoverCardTrigger({
  ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
  return (
    <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
  )
}

function HoverCardContent({
  className,
  align = "center",
  sideOffset = 4,
  ...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
  return (
    <HoverCardPrimitive.Portal data-slot="hover-card-portal">
      <HoverCardPrimitive.Content
        data-slot="hover-card-content"
        align={align}
        sideOffset={sideOffset}
        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-(--radix-hover-card-content-transform-origin) outline-hidden",
          className
        )}
        {...props}
      />
    </HoverCardPrimitive.Portal>
  )
}

export { HoverCard, HoverCardTrigger, HoverCardContent }

export default function HoverCardDemo({
  followCursor = false,
  side = "bottom",
  sideOffset = 4,
  align = "center",
  alignOffset = 0,
}: {
  followCursor?: boolean | "x" | "y"
  side?: "top" | "right" | "bottom" | "left"
  sideOffset?: number
  align?: "start" | "center" | "end"
  alignOffset?: number
}) {
  return (
    <HoverCard openDelay={10} closeDelay={100} followCursor={followCursor}>
      <HoverCardTrigger asChild>
        <Button variant="link">Hover Here</Button>
      </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

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 Radix UI. Everything else stays the same.

import { HoverCard } from '@animate-ui/react/primitives/radix-ui';

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

Coming from the @radix-ui/react-hover-card package? Use the subpath: import * as HoverCardPrimitive from '@animate-ui/react/primitives/radix-ui/hover-card'.

Animation

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

  • Content grows from 0.8 and fades in, from the trigger (--radix-hover-card-content-transform-origin), on a spring (stiffness 400, damping 29) that overshoots a little before it settles.
  • Once Radix'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.
  • Radix only opens it for mouse-like pointers: a tap on a touch screen follows the link, nothing sticks.
  • With forceMount (on the Content or the Portal), the closed content stays in the DOM: it plays its exit, then is hidden and inert.
  • 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 content 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 Radix UI's Hover Card. Animate UI adds the animation props below.

Root

Radix UI props, plus:

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

Trigger, Portal, Arrow

Radix UI props only. They are not animated.

Content

Radix 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