# Preview Card

A non-modal rich preview that opens on hover or focus of an inline link.

## Installation

```tsx
import {
  PreviewCard,
  PreviewCardTrigger,
  PreviewCardContent,
  PreviewCardBody,
  PreviewCardTitle,
  PreviewCardDescription,
} from "@makeplane/propel/components/preview-card";
```
## Usage

```tsx
import {
  PreviewCard,
  PreviewCardBody,
  PreviewCardContent,
  PreviewCardDescription,
  PreviewCardTitle,
  PreviewCardTrigger,
} from "@makeplane/propel/components/preview-card";

export default function BasicDemo() {
  return (
    <PreviewCard>
      <PreviewCardTrigger href="https://plane.so">Plane</PreviewCardTrigger>
      <PreviewCardContent side="top">
        <PreviewCardBody>
          <PreviewCardTitle>Plane</PreviewCardTitle>
          <PreviewCardDescription>
            Open-source project management for issues, sprints, and roadmaps.
          </PreviewCardDescription>
        </PreviewCardBody>
      </PreviewCardContent>
    </PreviewCard>
  );
}
```
## Examples

### With image

```tsx
import {
  PreviewCard,
  PreviewCardBody,
  PreviewCardContent,
  PreviewCardDescription,
  PreviewCardImage,
  PreviewCardTitle,
  PreviewCardTrigger,
} from "@makeplane/propel/components/preview-card";

export default function WithImageDemo() {
  return (
    <PreviewCard>
      <PreviewCardTrigger href="https://plane.so/brand">brand guidelines</PreviewCardTrigger>
      <PreviewCardContent sideOffset={8}>
        <PreviewCardImage
          src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=296&h=198&fit=crop"
          alt="Station signage set in large, high-contrast lettering"
          width={296}
          height={198}
        />
        <PreviewCardBody>
          <PreviewCardTitle>Brand guidelines</PreviewCardTitle>
          <PreviewCardDescription>
            Typography, color, and logo usage for everything Plane ships.
          </PreviewCardDescription>
        </PreviewCardBody>
      </PreviewCardContent>
    </PreviewCard>
  );
}
```

### With properties

```tsx
import { Avatar } from "@makeplane/propel/components/avatar";
import { Icon } from "@makeplane/propel/components/icon";
import { Pill } from "@makeplane/propel/components/pill";
import {
  PreviewCard,
  PreviewCardBody,
  PreviewCardContent,
  PreviewCardDescription,
  PreviewCardEyebrow,
  PreviewCardEyebrowLabel,
  PreviewCardPropertyGroup,
  PreviewCardTitle,
  PreviewCardTrigger,
} from "@makeplane/propel/components/preview-card";
import { CircleDashedOutline, DotOutline, PriorityHigh } from "@makeplane/propel/icons";

export default function WithPropertiesDemo() {
  return (
    <PreviewCard>
      <PreviewCardTrigger href="https://app.plane.so/issues/WEB-142">WEB-142</PreviewCardTrigger>
      <PreviewCardContent side="top">
        <PreviewCardBody>
          <PreviewCardEyebrow>
            {/* Figma's issue frame uses a 16×16 filled type indicator here; Icon is a stand-in. */}
            <Icon icon={DotOutline} tint="secondary" size="md" />
            <PreviewCardEyebrowLabel>WEB-142</PreviewCardEyebrowLabel>
          </PreviewCardEyebrow>
          <PreviewCardTitle>Redesign the pricing page</PreviewCardTitle>
          <PreviewCardDescription>
            Rework the tiered layout to highlight the annual plan discount.
          </PreviewCardDescription>
          <PreviewCardPropertyGroup>
            <Pill size="xs" startIcon={<Icon icon={CircleDashedOutline} />} label="Draft" />
            <Pill size="xs" startIcon={<Icon icon={PriorityHigh} />} label="High" />
            <Avatar
              size="xs"
              alt="Priya Sharma"
              src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=40&h=40&fit=crop&crop=faces"
              fallback="P"
            />
          </PreviewCardPropertyGroup>
        </PreviewCardBody>
      </PreviewCardContent>
    </PreviewCard>
  );
}
```

### Controlled

A controlled card owns its own visibility through `open` / `onOpenChange`, and `triggerId` selects which trigger it anchors to — letting application code open the card programmatically.

```tsx
import { Button } from "@makeplane/propel/components/button";
import {
  PreviewCard,
  PreviewCardBody,
  PreviewCardContent,
  PreviewCardDescription,
  PreviewCardTitle,
  PreviewCardTrigger,
} from "@makeplane/propel/components/preview-card";
import * as React from "react";

const topics: Record<string, { title: string; description: string }> = {
  cycles: {
    title: "Cycles",
    description: "Time-boxed iterations for planning and shipping work in focused sprints.",
  },
  modules: {
    title: "Modules",
    description: "Larger bodies of related work items tracked together across cycles.",
  },
};

export default function ControlledDemo() {
  const [open, setOpen] = React.useState(false);
  const [triggerId, setTriggerId] = React.useState<string | null>(null);
  return (
    <div className="flex flex-col items-start gap-4">
      <PreviewCard
        open={open}
        triggerId={triggerId}
        onOpenChange={(nextOpen, eventDetails) => {
          setOpen(nextOpen);
          setTriggerId(eventDetails.trigger?.id ?? null);
        }}
      >
        {({ payload }) => {
          const topic = typeof payload === "string" ? topics[payload] : undefined;
          return (
            <>
              <p className="max-w-prose text-body-sm-regular text-secondary">
                Group sprint work into{" "}
                <PreviewCardTrigger
                  id="cycles-trigger"
                  payload="cycles"
                  href="https://docs.plane.so/core-concepts/cycles"
                >
                  cycles
                </PreviewCardTrigger>{" "}
                or feature work into{" "}
                <PreviewCardTrigger
                  id="modules-trigger"
                  payload="modules"
                  href="https://docs.plane.so/core-concepts/modules"
                >
                  modules
                </PreviewCardTrigger>
                .
              </p>
              <PreviewCardContent sideOffset={8}>
                {topic === undefined ? null : (
                  <PreviewCardBody>
                    <PreviewCardTitle>{topic.title}</PreviewCardTitle>
                    <PreviewCardDescription>{topic.description}</PreviewCardDescription>
                  </PreviewCardBody>
                )}
              </PreviewCardContent>
            </>
          );
        }}
      </PreviewCard>
      <Button
        variant="secondary"
        size="md"
        stretch="auto"
        onClick={() => {
          setTriggerId("modules-trigger");
          setOpen(true);
        }}
        label="Preview modules"
      />
    </div>
  );
}
```

### Detached triggers

`createPreviewCardHandle()` links triggers outside the `PreviewCard` root to it via the `handle` prop; each trigger passes a `payload` the card renders through function-as-children, so one card serves several links.

```tsx
import {
  createPreviewCardHandle,
  PreviewCard,
  PreviewCardBody,
  PreviewCardContent,
  PreviewCardDescription,
  PreviewCardTitle,
  PreviewCardTrigger,
} from "@makeplane/propel/components/preview-card";

const topics: Record<string, { title: string; description: string }> = {
  cycles: {
    title: "Cycles",
    description: "Time-boxed iterations for planning and shipping work in focused sprints.",
  },
  modules: {
    title: "Modules",
    description: "Larger bodies of related work items tracked together across cycles.",
  },
};

const topicPreviewCard = createPreviewCardHandle();

export default function DetachedTriggersDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <p className="max-w-prose text-body-sm-regular text-secondary">
        Plan sprint work with{" "}
        <PreviewCardTrigger
          handle={topicPreviewCard}
          payload="cycles"
          href="https://docs.plane.so/core-concepts/cycles"
        >
          cycles
        </PreviewCardTrigger>{" "}
        and group feature work with{" "}
        <PreviewCardTrigger
          handle={topicPreviewCard}
          payload="modules"
          href="https://docs.plane.so/core-concepts/modules"
        >
          modules
        </PreviewCardTrigger>
        .
      </p>
      <PreviewCard handle={topicPreviewCard}>
        {({ payload }) => {
          const topic = typeof payload === "string" ? topics[payload] : undefined;
          return (
            <PreviewCardContent sideOffset={8}>
              {topic === undefined ? null : (
                <PreviewCardBody>
                  <PreviewCardTitle>{topic.title}</PreviewCardTitle>
                  <PreviewCardDescription>{topic.description}</PreviewCardDescription>
                </PreviewCardBody>
              )}
            </PreviewCardContent>
          );
        }}
      </PreviewCard>
    </div>
  );
}
```
## API Reference

### PreviewCard

The preview-card Root — Base UI's context/state provider (renders no element of its own). A
behavior-only role, so it lives in `components` (rules 1a, 2); the styled parts live in
`elements/preview-card` (and shared `internal/` primitives) and are grafted onto Base UI behavior
here.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode \| PayloadChildRenderFunction<unknown>` | — | The content of the preview card. This can be a regular React node or a render function that receives the `payload` of the active trigger. |

### PreviewCardTrigger

The link that opens the preview card on hover or focus. Renders an `<a>` by default; pass
`render` to project the trigger onto your own link. Maps 1:1 to `PreviewCard.Trigger`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, PreviewCardTriggerState>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### PreviewCardContent

Convenience that composes the anchored preview card boilerplate — Base UI portal, positioner, and
popup grafted onto Propel's styled surface — so a consumer only writes the trigger and the card
body. Pass `side`/`sideOffset`/`align` through to the positioner.

No backdrop: unlike Dialog/AlertDialog/Drawer, a preview card is a non-modal, hover-triggered
rich tooltip (it opens on hover/focus and closes on pointer-leave/blur, with no focus trap) —
dimming the page behind a hover preview reads as a modal takeover it isn't. Base UI ships a
`PreviewCard.Backdrop` primitive, but it is purely optional/decorative for apps that want one; it
has none of `Dialog.Backdrop`'s click-outside-to-dismiss semantics, so omitting it costs no
behavior.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
| `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-end" \| "inline-start"` | `bottom` | Which side of the trigger the card opens toward. |
| `sideOffset` | `number \| OffsetFunction` | `4` | Distance in px between the trigger and the card. |
| `align` | `"center" \| "start" \| "end"` | `center` | Alignment of the card relative to the trigger along `side`. |
| `alignOffset` | `number \| OffsetFunction` | `0` | Additional offset in px along the align axis — slides the card along the trigger's edge without changing `align`. |
| `collisionPadding` | `Padding` | `5` | Minimum gap in px between the card and the viewport edge it would otherwise touch. |
| `collisionBoundary` | `Boundary` | — | The element or rect the card is confined to when avoiding collisions. Base UI's clipping ancestors when unset. |
| `collisionAvoidance` | `CollisionAvoidance` | — | What to do when the card would overflow the boundary, set per axis: flip to the other side, shift along it, or stay put. Base UI flips the side and shifts the alignment when unset. |
| `sticky` | `boolean` | `false` | Keep the card on screen after the trigger scrolls out of view, instead of following it out. |
| `positionMethod` | `"absolute" \| "fixed"` | `"absolute"` | Which CSS `position` the positioner uses. `fixed` escapes overflow-clipping ancestors, e.g. a trigger inside a sticky header. |
| `anchor` | `Element \| VirtualElement \| RefObject<Element \| null> \| (() => Element \| VirtualElement \| null) \| null` | — | Position against this element instead of the trigger — an element, a ref, a virtual element, or a function returning one. Geometry only; the trigger keeps its role and interactions. |
| `disableAnchorTracking` | `boolean` | `false` | Stop tracking layout shifts of the anchor — position once, on open. |

### PreviewCardBody

The text content area of the card — typically holds a `PreviewCardTitle` and
`PreviewCardDescription` stacked in a column. Owns the padding so a full-bleed `PreviewCardImage`
can sit edge-to-edge above it; both the column layout and the padding are "always the same" per
the design spec.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### PreviewCardTitle

The card's primary heading — `text-body-sm-medium` per the design spec. Truncates to a single
line instead of wrapping (Figma titles are nowrap on the 296px card). Diverges from the shared
`internal/overlay-title` "md" step (`semibold`) — that step has no current consumer (this family
used to adopt it) and is lighter `medium` here per Figma, so it keeps its own recipe (rule 4a:
divergent styling stays per-family). When `children` is a string, bakes a native `title` so hover
recovers the full text past the truncation; pass `title` explicitly to override (or `title=""` to
suppress).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### PreviewCardDescription

Supporting description text beneath the title — the shared `internal/overlay-description` recipe
at the preview card's `md` size (13px secondary text). The size is fixed here (a `components`
default, rule 13).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### PreviewCardImage

The thumbnail image shown inside the popup. Bakes in `w-full`, overflow-hidden, and object-cover
so the thumbnail always spans the card, clips, and fills its box — these are "always the same"
per the design spec. Height is set by the consumer's layout (native `height`, or the image's
intrinsic aspect ratio at full width).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### PreviewCardEyebrow

Lays an optional leading node beside text on one row. The slot is unstyled flex — pass an icon, a
16×16 filled type indicator (Figma's issue frame), or any other leading glyph. Stories use the
shared `internal/Icon` at `tint="secondary" size="md"` as a stand-in. Issue cards put a
`PreviewCardEyebrowLabel` identifier (an issue key) here ABOVE a separate `PreviewCardTitle`.
Cycle / module / release / intake cards put `PreviewCardTitle` in this row and skip
`PreviewCardEyebrowLabel` — those canvases have no muted identifier. Compose it only when the
card has a leading glyph; a card without one renders `PreviewCardTitle` directly inside
`PreviewCardBody`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### PreviewCardEyebrowLabel

The single-line identifier text inside a `PreviewCardEyebrow` (an issue key, …). Muted one step
further than the description and truncates instead of wrapping. Not the card's heading — cycle /
module / release / intake names belong in `PreviewCardTitle` (optionally inside the eyebrow,
beside the icon). When `children` is a string, bakes a native `title` so hover recovers the full
text past the truncation; pass `title` explicitly to override (or `title=""` to suppress).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### PreviewCardPropertyGroup

The row of property chips (status, priority, assignee, labels, …) shown beneath the description.
A bare flex-wrap row — the chips are the consumer's own components (`Pill`, `Avatar`, `Badge`,
…); this part only supplies the row layout. Never `aria-hidden`: its content is meaningful.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |

### PreviewCardMeta

The muted footer caption closing out the card's text content (a source domain, a relative
timestamp, …).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, {}>` | — | Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render. |
