# Hover Card

For sighted users to preview content available behind a link.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
import { HStack, VStack } from "@/components/ui/stack"
import { color, leading, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  name: { fontWeight: 600 },
  meta: { fontSize: text.xs, lineHeight: leading.xs, color: color.mutedForeground },
})

export default function HoverCardDemo() {
  return (
    <HoverCard>
      <HoverCardTrigger delay={200} closeDelay={100} render={<Button variant="link" />}>
        @nextjs
      </HoverCardTrigger>
      <HoverCardContent>
        <HStack gap={3}>
          <Avatar>
            <AvatarImage src="https://github.com/vercel.png" alt="@nextjs" />
            <AvatarFallback>NX</AvatarFallback>
          </Avatar>
          <VStack gap={0.5}>
            <span {...stylex.props(styles.name)}>@nextjs</span>
            <span>The React framework, created and maintained by @vercel.</span>
            <span {...stylex.props(styles.meta)}>Joined December 2021</span>
          </VStack>
        </HStack>
      </HoverCardContent>
    </HoverCard>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and `state-styles.ts` from the same page.
   The card's opaque box is the shared popup surface. `state-styles.ts` reads the popup's transition state.

3. Copy and paste the following code into your project.

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

   import type { CSSProperties } from "react"
   import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"

   import { stateStyles } from "@/lib/state-styles"
   import { states } from "@/theme/states"
   import { duration, ease, layer, leading, radius, space, text } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     hidden: { opacity: 0 },
     content: {
       zIndex: layer.popup,
       width: "16rem",
       maxWidth: "var(--available-width)",
       borderRadius: radius.xl,
       padding: space.s2_5,
       fontSize: text.sm,
       lineHeight: leading.sm,
       transformOrigin: "var(--transform-origin)",
       transitionProperty: "opacity, transform",
       transitionDuration: {
         default: duration.fade,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.standard,
     },
   })

   const slide = stylex.create({
     top: { transform: `scale(0.95) translateY(${space.s2})` },
     bottom: { transform: `scale(0.95) translateY(calc(-1 * ${space.s2}))` },
     left: { transform: `scale(0.95) translateX(${space.s2})` },
     right: { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
     "inline-start": { transform: `scale(0.95) translateX(${space.s2})` },
     "inline-end": { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
   })

   type Overrides = { xstyle?: StyleXStyles; className?: string }
   type StateOverrides = Overrides & { style?: CSSProperties }

   function HoverCard<Payload>(props: PreviewCardPrimitive.Root.Props<Payload>) {
     return <PreviewCardPrimitive.Root {...props} />
   }

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

   type HoverCardContentProps = Omit<PreviewCardPrimitive.Popup.Props, "className" | "style"> &
     Pick<PreviewCardPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset"> &
     StateOverrides

   function HoverCardContent({
     xstyle,
     style,
     className,
     side = "bottom",
     sideOffset = 4,
     align = "center",
     alignOffset = 0,
     ...props
   }: HoverCardContentProps) {
     return (
       <PreviewCardPrimitive.Portal>
         <PreviewCardPrimitive.Positioner
           align={align}
           alignOffset={alignOffset}
           side={side}
           sideOffset={sideOffset}
           {...stylex.props(states.positioner)}
         >
           <PreviewCardPrimitive.Popup
             data-slot="hover-card-content"
             {...props}
             {...stateStyles(
               (state) => [
                 states.popupSurface,
                 styles.content,
                 (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                   styles.hidden,
                 (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                   slide[state.side],
                 xstyle,
               ],
               { className, style },
             )}
           />
         </PreviewCardPrimitive.Positioner>
       </PreviewCardPrimitive.Portal>
     )
   }

   export { HoverCard, HoverCardContent, HoverCardTrigger }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
```

```tsx
<HoverCard>
  <HoverCardTrigger href="https://github.com/vercel">@nextjs</HoverCardTrigger>
  <HoverCardContent>The React framework, created and maintained by @vercel.</HoverCardContent>
</HoverCard>
```

## Composition

```text
HoverCard
├── HoverCardTrigger
└── HoverCardContent      portal + positioner + popup
```

`HoverCardTrigger` renders an `<a>`. Pass `render` to anchor the card to a
different element.

> **Warning**
>
> A hover card opens on hover and on keyboard focus, never on press. A touch
> screen has no hover, so the card never opens there. Put nothing in it that a
> reader cannot also reach another way.

## Trigger delays

Use `delay` and `closeDelay` on the trigger to set how long the pointer has to
rest before the card opens, and how long it stays open after the pointer leaves.
`closeDelay` keeps the card open while the pointer crosses the gap into it.

```tsx
import { Button } from "@/components/ui/button"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
import { HStack } from "@/components/ui/stack"

export default function HoverCardDelay() {
  return (
    <HStack gap={2} wrap="wrap" justify="center">
      <HoverCard>
        <HoverCardTrigger delay={0} closeDelay={0} render={<Button variant="outline" />}>
          Immediate
        </HoverCardTrigger>
        <HoverCardContent>Opens the moment the pointer lands.</HoverCardContent>
      </HoverCard>
      <HoverCard>
        <HoverCardTrigger render={<Button variant="outline" />}>Default</HoverCardTrigger>
        <HoverCardContent>Waits 600ms to open and 300ms to close.</HoverCardContent>
      </HoverCard>
      <HoverCard>
        <HoverCardTrigger delay={1000} closeDelay={600} render={<Button variant="outline" />}>
          Patient
        </HoverCardTrigger>
        <HoverCardContent>Waits a second, then stays while you cross the gap.</HoverCardContent>
      </HoverCard>
    </HStack>
  )
}
```

## Sides

Use `side` on `HoverCardContent` to pick the edge it opens from, and `align` to
slide it along that edge. Base UI flips the side when there is no room.

```tsx
import { Button } from "@/components/ui/button"
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"
import { HStack, VStack } from "@/components/ui/stack"

const sides = ["top", "right", "bottom", "left"] as const

export default function HoverCardSides() {
  return (
    <HStack gap={2} wrap="wrap" justify="center">
      {sides.map((side) => (
        <HoverCard key={side}>
          <HoverCardTrigger delay={100} closeDelay={100} render={<Button variant="outline" />}>
            {side}
          </HoverCardTrigger>
          <HoverCardContent side={side}>
            <VStack gap={1}>
              <span>Preview</span>
              <span>This card opens on the {side} of its trigger.</span>
            </VStack>
          </HoverCardContent>
        </HoverCard>
      ))}
    </HStack>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part wraps the Base UI `PreviewCard` part of the same name and accepts its
props. See the
[Base UI reference](https://base-ui.com/react/components/preview-card) for the
full list. Each styled part also takes `xstyle` and `className`.

### HoverCard

| Prop           | Type                               | Default |
| -------------- | ---------------------------------- | ------- |
| `open`         | `boolean`                          | —       |
| `defaultOpen`  | `boolean`                          | `false` |
| `onOpenChange` | `(open: boolean, details) => void` | —       |
| `handle`       | `PreviewCardHandle`                | —       |

### HoverCardTrigger

| Prop         | Type      | Default |
| ------------ | --------- | ------- |
| `delay`      | `number`  | `600`   |
| `closeDelay` | `number`  | `300`   |
| `payload`    | `Payload` | —       |

### HoverCardContent

| Prop          | Type                                     | Default    |
| ------------- | ---------------------------------------- | ---------- |
| `side`        | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` |
| `sideOffset`  | `number`                                 | `4`        |
| `align`       | `"start" \| "center" \| "end"`           | `"center"` |
| `alignOffset` | `number`                                 | `0`        |

The card is `16rem` wide and clamped to `--available-width`, so it narrows
rather than overflowing a phone-width viewport. Change the width through
`xstyle`.

### Data attributes

| Attribute                                  | On        | Effect                                      |
| ------------------------------------------ | --------- | ------------------------------------------- |
| `data-slot="hover-card-content"`           | the popup | Popup surface                               |
| `data-side`                                | the popup | Which way it slides in from                 |
| `data-starting-style`, `data-ending-style` | the popup | Base UI writes them while the card animates |
| `data-open`, `data-closed`                 | the popup | Base UI writes them                         |
