# Hover Card

> Delayed hover and focus preview card for profiles and link previews.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/hover-card
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/hover-card.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/hover-card-demo.tsx"
"use client"

import { CalendarDays } from "lucide-react"

import { HoverCard } from "@/components/hover-card"

export default function HoverCardDemo() {
  return (
    <div className="flex min-h-64 items-start justify-center pt-16">
      <p className="max-w-xs text-sm leading-relaxed text-muted-foreground">
        The framework behind this site is maintained by{" "}
        <HoverCard
          width={288}
          trigger={
            <a href="#vercel" onClick={(event) => event.preventDefault()} className="font-medium text-foreground underline underline-offset-4">
              @vercel
            </a>
          }
        >
          <div className="flex gap-3">
            <span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-foreground text-sm font-bold text-background">▲</span>
            <div className="grid gap-1">
              <p className="text-sm font-semibold">@vercel</p>
              <p className="text-sm">Develop. Preview. Ship. The platform for frontend developers.</p>
              <p className="flex items-center gap-1.5 pt-1 text-xs text-muted-foreground">
                <CalendarDays className="size-3.5" /> Joined December 2021
              </p>
            </div>
          </div>
        </HoverCard>{" "}
        and a lot of contributors.
      </p>
    </div>
  )
}
```

## Source

```tsx title="components/hover-card.tsx"
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

export interface HoverCardProps extends Omit<React.ComponentProps<"span">, "children"> {
  /** Element that reveals the card, usually a link */
  trigger: React.ReactNode
  /** Card content */
  children: React.ReactNode
  openDelay?: number
  closeDelay?: number
  side?: "top" | "bottom"
  align?: "start" | "center" | "end"
  width?: number
}

const sides = { top: "bottom-full pb-2 origin-bottom", bottom: "top-full pt-2 origin-top" } as const
const aligns = { start: "left-0", center: "left-1/2 -translate-x-1/2", end: "right-0" } as const

export function HoverCard({
  trigger,
  children,
  openDelay = 200,
  closeDelay = 150,
  side = "bottom",
  align = "center",
  width = 288,
  className,
  ...props
}: HoverCardProps) {
  const [open, setOpen] = React.useState(false)
  const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null)
  const id = React.useId()

  const schedule = (next: boolean) => {
    if (timer.current) clearTimeout(timer.current)
    timer.current = setTimeout(() => setOpen(next), next ? openDelay : closeDelay)
  }

  React.useEffect(
    () => () => {
      if (timer.current) clearTimeout(timer.current)
    },
    []
  )

  return (
    <span
      className={cn("relative inline-block", className)}
      aria-describedby={open ? id : undefined}
      onPointerEnter={() => schedule(true)}
      onPointerLeave={() => schedule(false)}
      onFocus={() => schedule(true)}
      onBlur={(event) => {
        if (!event.currentTarget.contains(event.relatedTarget)) schedule(false)
      }}
      onKeyDown={(event) => {
        if (event.key === "Escape") schedule(false)
      }}
      {...props}
    >
      {trigger}
      {open && (
        // The padding bridges the gap so the pointer can travel from trigger to card
        <span id={id} role="tooltip" className={cn("absolute z-20 block", sides[side], aligns[align])} style={{ width }}>
          <span className="block animate-in rounded-xl border bg-popover p-4 text-popover-foreground shadow-lg duration-150 fade-in zoom-in-95">
            {children}
          </span>
        </span>
      )}
    </span>
  )
}
```
