Hover card
A preview card that opens when a link is hovered or focused, for content sighted users can glance at.
Used in Pro blocks: Chat Sidebar and Thinking
pnpm dlx shadcn@latest add https://hextaui.com/r/hover-card.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @base-ui/react cnCopy and paste the following code into your project.
components/ui/hover-card.tsx lib/motion.ts Update the import paths to match your project setup.
A hover card is a preview, not a menu or a dialog. The trigger stays a normal link, so everything in the card must also be on the page it links to.
Set side and align on <HoverCardContent />. Logical sides like inline-end follow the reading direction, and the card flips or shifts when it would leave the screen.
delay and closeDelay on the trigger set how long the pointer must rest before the card opens and how long it lingers after leaving. The 600ms default stops cards from flashing open as the pointer crosses a page.
Use render to make the trigger any link, including one inside a sentence. When a link wraps onto two lines, the card anchors to the line you hovered.
Move the pointer from the link into the card and it stays open, so links and buttons inside can be clicked. The path between them is forgiving, so a diagonal move doesn’t close it.
One card serves many links. Create a handle with createHoverCardHandle, give each trigger a payload, and read it in the card. Moving between names glides the card to the new link instead of closing and reopening it. The old content slides out the way you moved, the new content slides in, and the height eases between the two.
arrow adds a pointer that joins the card's border without a seam. The side offset grows to make room for it, and it follows the card when it flips.
Start fetching in onOpenChange and show a skeleton until the data arrives. When the content changes, the card eases to its new height instead of jumping.
Pass open and onOpenChange. The second argument says why it changed, such as trigger-hover, trigger-focus or escape-key.
Unbroken text wraps inside the card, and a card taller than the space beside the trigger scrolls instead of leaving the screen.
The card reads the trigger’s direction, so logical sides and alignment flip and the scale animation grows from the correct corner.
| Key | Action |
|---|---|
| Tab | Focusing the trigger opens the card after the same delay as hover. Moving focus on closes it. |
| Enter | Follows the link, like any other link. |
| Esc | Closes the card. |
- The card is a visual extra for sighted mouse and keyboard users. Screen readers only hear the link, so they aren’t forced through a preview on every link they pass.
- Nothing opens on touch screens, where there is no hover. A tap follows the link, which is why the destination has to hold the same information.
- Focus never moves into the card. If it needs controls that must be reachable from the keyboard, use a popover instead.
- With reduced motion on, the card fades without scaling, and a shared card jumps between links instead of gliding.
Built on the Base UI preview card. Every part accepts the props of the primitive it wraps.
| Prop | Type | Default |
|---|---|---|
open | boolean | – |
defaultOpen | boolean | false |
onOpenChangedetails.reason is trigger-hover, trigger-focus, trigger-press, outside-press, escape-key, imperative-action or none. | (open: boolean, details) => void | – |
onOpenChangeCompleteCalled after the open or close animation ends. | (open: boolean) => void | – |
handleConnects triggers rendered outside the root. | HoverCardHandle<Payload> | – |
childrenUse the function form to read the payload of the trigger that opened the card. | ReactNode | ({ payload }) => ReactNode | – |
actionsRef | RefObject<{ close, unmount }> | – |
| Prop | Type | Default |
|---|---|---|
href | string | – |
delayMilliseconds before hover or focus opens the card. | number | 600 |
closeDelayMilliseconds the card stays open after leaving. | number | 300 |
handle | HoverCardHandle<Payload> | – |
payloadPassed to the card when this trigger opens it. | Payload | – |
renderRender your own link, such as <Button variant="link" /> or a router link. | ReactElement | (props, state) => ReactElement | <a> |
| Attribute | Description |
|---|---|
data-slot="hover-card-trigger" | Target the trigger in CSS. |
data-popup-open | Present while this trigger’s card is open. |
| Prop | Type | Default |
|---|---|---|
side | "top" | "bottom" | "left" | "right" | "inline-start" | "inline-end" | "bottom" |
align | "start" | "center" | "end" | "center" |
arrowShow a pointer toward the trigger. | boolean | false |
sideOffset | number | OffsetFunction | 6, or 10 with arrow |
alignOffset | number | OffsetFunction | 0 |
collisionPaddingSpace kept between the card and the viewport edge. | number | Rect | 8 |
collisionAvoidanceWhether the card flips, shifts or does nothing on collision. | CollisionAvoidance | – |
sticky | boolean | false |
anchorPosition against something other than the trigger. | Element | RefObject | VirtualElement | – |
positionMethod | "absolute" | "fixed" | "absolute" |
disableAnchorTracking | boolean | false |
portalPropsProps for the portal, such as container. | HoverCardPortalProps | – |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="hover-card-content" | Target the card in CSS. |
data-open | Present while the card is open. |
data-starting-style | Present while the card animates in. |
data-ending-style | Present while the card animates out. |
data-instant | focus when keyboard focus opened the card, dismiss when Escape or an outside press closed it. The exit animation is skipped while it’s set. |
data-side | The side the card settled on after collisions. |
data-align | The alignment it settled on. |
--transform-origin | Where the scale animation grows from, next to the trigger. |
--available-width | Room left beside the trigger. The card never grows past it. |
--available-height | Room left above or below. Taller content scrolls. |
| Positioner attribute | Description |
|---|---|
data-slot="hover-card-positioner" | The element that moves. It glides when a shared card switches links. |
data-anchor-hidden | Present when the trigger scrolls out of view. |
| Inner parts | Description |
|---|---|
data-slot="hover-card-viewport" | Wraps the content. Carries data-activation-direction while a shared card switches links. |
data-slot="hover-card-body" | Your content. Its height eases when it changes. |
data-slot="hover-card-arrow" | The pointer, with data-side for its edge. |
--popup-height | Set on the card while it resizes between links. |
| Prop | Type | Default |
|---|---|---|
container | HTMLElement | ShadowRoot | RefObject | null | document.body |
keepMounted | boolean | false |
Returns a handle for detached triggers. Its open(triggerId) and close() methods control the card from event handlers, and isOpen reads its state. Pass a type argument to type the payload.
- MotionThe easing curves, durations and reduced-motion check every component animates with, plus hooks for size morphs and sliding highlights.
- Alert dialogA confirmation dialog for destructive or important actions that waits for async work, and becomes a bottom sheet on phones.
- CommandA searchable list of actions, inline or as a ⌘K palette, with pages, shortcuts and highlighted matches.
- Context menuA menu of actions on right click or long press, with submenus, checkbox and radio items, and hold feedback on touch.
- DialogA window over the page for forms and focused tasks, with a pinned header and footer, nesting, and a swipeable bottom sheet on phones.
- DrawerA panel that slides in from any edge and follows your finger, with snap points, a working handle and nested drawers that stack.
Used in Pro blocks
Blocks that build on Hover card.
- Chat SidebarThe sidebar of a chat app, with pinned chats, projects and recents. Handles rename in place, delete with undo and chats still replying.
- ThinkingThe thinking view of an AI chat. A live label follows each step, searches and sources fold into a trail, and citations sit beside the answer.
Every block comes with HextaUI Pro, and Prompt Input is free.