Hover Card

A hover card previews where a link goes. Hover or focus the link and a small card shows who or what it points to, without leaving the page.

Built by @njoh-simplice with Base UI.

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";

export default function HoverCardDemo() {
  return (
    <p className="max-w-sm text-center text-base">
      Built by{" "}
      <HoverCard>
        <HoverCardTrigger href="https://github.com/njoh-simplice">@njoh-simplice</HoverCardTrigger>
        <HoverCardContent>
          <div className="flex gap-3">
            <Avatar>
              <AvatarImage
                src="/assets/images/profile.webp"
                alt=""
                className="origin-[50%_38%] translate-y-[12%] scale-150 saturate-20"
              />
              <AvatarFallback>NS</AvatarFallback>
            </Avatar>
            <div className="flex flex-col gap-1">
              <p className="text-base font-semibold">Njoh Simplice Junior</p>
              <p className="text-sm text-text/70">Builds nsdev/ui, a component library on Base UI and Tailwind.</p>
            </div>
          </div>
        </HoverCardContent>
      </HoverCard>{" "}
      with Base UI.
    </p>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

import type { ComponentProps } from "react";
import { PreviewCard as BasePreviewCard } from "@base-ui/react/preview-card";
import { cx } from "@/lib/cx";
import { focusRing, popupMotion, popupSurface } from "@/lib/styles";
import type { WithClass } from "@/lib/types";

// A card that opens when you hover or focus a link, to preview where it goes. It is built on
// Base UI's PreviewCard.
export const HoverCard = BasePreviewCard.Root;

// A real link (an <a>), underlined. Give it an href.
export function HoverCardTrigger({ className, ...props }: WithClass<ComponentProps<typeof BasePreviewCard.Trigger>>) {
  return (
    <BasePreviewCard.Trigger
      className={cx(
        "rounded-sm text-text underline decoration-text-secondary underline-offset-4 transition-colors hover:decoration-text",
        focusRing,
        className,
      )}
      {...props}
    />
  );
}

export function HoverCardContent({
  side = "bottom",
  sideOffset = 8,
  align = "center",
  className,
  ...props
}: WithClass<ComponentProps<typeof BasePreviewCard.Popup>> &
  Pick<ComponentProps<typeof BasePreviewCard.Positioner>, "side" | "sideOffset" | "align">) {
  return (
    <BasePreviewCard.Portal>
      <BasePreviewCard.Positioner side={side} sideOffset={sideOffset} align={align} className="z-50">
        <BasePreviewCard.Popup className={cx(popupSurface, popupMotion, "w-72 p-4", className)} {...props} />
      </BasePreviewCard.Positioner>
    </BasePreviewCard.Portal>
  );
}

Props

PropTypeDefaultWhat it does
hrefstring—On HoverCardTrigger. Where the link goes. It is a real link, so it works without the card.
side"top" | "right" | "bottom" | "left""bottom"On HoverCardContent. Which side of the link it opens on. It flips to the other side if there is no room.
align"start" | "center" | "end""center"On HoverCardContent. How it lines up along that side.
delaynumber600On HoverCardTrigger. Milliseconds to wait after hovering before it opens.
closeDelaynumber300On HoverCardTrigger. Milliseconds to wait after leaving before it closes.

HoverCard is built on Base UI's PreviewCard. Every other prop goes to it.

The thinking

A hover card is a courtesy to people who want a quick look before they commit to a click. The page works exactly the same without it.

  • Built on Base UI: It uses Base UI's PreviewCard, which opens the card on hover and focus, keeps it open while the pointer is over it, and places it next to the link.
  • A real link: The trigger is an anchor with an href. If the card never opens, the link still goes where it should.
  • Preview, not interface: The card is for reading. Anything people need to click, such as a button, belongs in a Popover.

Interface design

  • Link: Underlined with a quiet line that turns to the main text color on hover, so it reads as a link in running text.
  • Card: The shared overlay surface, 18rem wide, with no arrow. The demo shows a profile with the avatar.

Interaction

  • Hover: The card opens after a short pause and stays open while the pointer moves from the link onto the card.
  • Focus: It opens when the link receives keyboard focus, and closes when focus leaves.
  • Click: Following the link works as usual.
  • Motion: A 150ms fade and a small scale from the side it opens from. With reduced motion turned on it appears at once.

Empathy

  • Nothing is lost without it: On touch screens there is no hover, so the card doesn't open. The link still works, so nothing the card says should be needed to use the page.
  • Link text: The link has its own readable text, such as a name, so it makes sense out of context.
  • Keyboard: Keyboard users get the card on focus, not only mouse users on hover.