Preview Card
A non-modal rich preview that opens on hover or focus of an inline link.
Basic
Show codeHide code
import {
PreviewCard,
PreviewCardArrow,
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>
<PreviewCardArrow />
</PreviewCardContent>
</PreviewCard>
);
}With image
Show codeHide code
import {
PreviewCard,
PreviewCardArrow,
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=448&h=300"
alt="Station signage set in large, high-contrast lettering"
width={320}
height={214}
/>
<PreviewCardBody>
<PreviewCardTitle>Brand guidelines</PreviewCardTitle>
<PreviewCardDescription>
Typography, color, and logo usage for everything Plane ships.
</PreviewCardDescription>
</PreviewCardBody>
<PreviewCardArrow />
</PreviewCardContent>
</PreviewCard>
);
}With properties
Show codeHide code
import { Avatar } from "@makeplane/propel/components/avatar";
import { Badge } from "@makeplane/propel/components/badge";
import {
PreviewCard,
PreviewCardArrow,
PreviewCardBody,
PreviewCardContent,
PreviewCardDescription,
PreviewCardIcon,
PreviewCardMeta,
PreviewCardPropertyGroup,
PreviewCardTitle,
PreviewCardTitleRow,
PreviewCardTrigger,
} from "@makeplane/propel/components/preview-card";
import { CircleDot } from "lucide-react";
export default function WithPropertiesDemo() {
return (
<PreviewCard>
<PreviewCardTrigger href="https://app.plane.so/issues/WEB-142">WEB-142</PreviewCardTrigger>
<PreviewCardContent side="top">
<PreviewCardBody>
<PreviewCardTitleRow>
<PreviewCardIcon>
<CircleDot />
</PreviewCardIcon>
<PreviewCardTitle>Redesign the pricing page</PreviewCardTitle>
</PreviewCardTitleRow>
<PreviewCardDescription>
Rework the tiered layout to highlight the annual plan discount.
</PreviewCardDescription>
<PreviewCardPropertyGroup>
<Badge tone="warning" magnitude="sm" label="In Progress" />
<Badge tone="danger" magnitude="sm" label="High priority" />
<Avatar magnitude="xs" alt="Priya Sharma" fallback="P" />
</PreviewCardPropertyGroup>
<PreviewCardMeta>WEB-142 · Updated 2 days ago</PreviewCardMeta>
</PreviewCardBody>
<PreviewCardArrow />
</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.
Show codeHide code
import { Button } from "@makeplane/propel/components/button";
import {
PreviewCard,
PreviewCardArrow,
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-14 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>
)}
<PreviewCardArrow />
</PreviewCardContent>
</>
);
}}
</PreviewCard>
<Button
prominence="secondary"
tone="neutral"
magnitude="lg"
sizing="hug"
onClick={() => {
setTriggerId("modules-trigger");
setOpen(true);
}}
label="Preview modules"
/>
</div>
);
}Installation
import {
PreviewCard,
PreviewCardTrigger,
PreviewCardContent,
PreviewCardBody,
PreviewCardTitle,
PreviewCardDescription,
PreviewCardArrow,
} from "@makeplane/propel/components/preview-card";
Props
PreviewCard
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | PayloadChildRenderFunction<unknown> | No | 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. |
PreviewCardContent
| Prop | Type | Required | Description |
|---|---|---|---|
| render | ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}> | No | 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" | No | Which side of the trigger the card opens toward. |
| sideOffset | number | OffsetFunction | No | Distance in px between the trigger and the card. |
| align | "center" | "start" | "end" | No | Alignment of the card relative to the trigger along `side`. |