Propel

Preview Card

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

Basic

Plane
Show 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 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 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.

Group sprint work into cycles or feature work into modules.

Show 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

PropTypeRequiredDescription
childrenReactNode | PayloadChildRenderFunction<unknown>NoThe 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

PropTypeRequiredDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>NoAllows 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"NoWhich side of the trigger the card opens toward.
sideOffsetnumber | OffsetFunctionNoDistance in px between the trigger and the card.
align"center" | "start" | "end"NoAlignment of the card relative to the trigger along `side`.