Siteplane UI

Card

Group related content and actions on a surface.

Markdown
Create project
Choose a name and template for your new project.
"use client";

import { CircleAlertIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Card,
  CardDescription,
  CardFooter,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";
import { Field, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { Input } from "@/siteplane/components/input";
import {
  Select,
  SelectPopup,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

const templates = [
  { label: "Blank project", value: "blank" },
  { label: "Marketing site", value: "marketing" },
  { label: "Dashboard", value: "dashboard" },
  { label: "Documentation", value: "docs" },
];

export default function Example() {
  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Create project</CardTitle>
        <CardDescription>
          Choose a name and template for your new project.
        </CardDescription>
      </CardHeader>
      <CardPanel>
        <Form
          className="flex w-full flex-col gap-4"
          onSubmit={(event) => event.preventDefault()}
        >
          <Field name="name">
            <FieldLabel>Name</FieldLabel>
            <Input placeholder="Name of your project" required type="text" />
          </Field>
          <Field name="template">
            <FieldLabel>Template</FieldLabel>
            <Select defaultValue="blank" items={templates}>
              <SelectTrigger>
                <SelectValue />
              </SelectTrigger>
              <SelectPopup>
                {templates.map(({ label, value }) => (
                  <SelectItem key={value} value={value}>
                    {label}
                  </SelectItem>
                ))}
              </SelectPopup>
            </Select>
          </Field>
          <Button className="w-full" type="submit">
            Create project
          </Button>
        </Form>
      </CardPanel>
      <CardFooter>
        <p className="flex gap-1 text-muted-foreground text-xs">
          <CircleAlertIcon className="size-3 h-lh shrink-0" />
          Setting up a project takes a few seconds.
        </p>
      </CardFooter>
    </Card>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/card

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/avatar @siteplane/button @siteplane/empty-state @siteplane/file-type-icon @siteplane/form @siteplane/input @siteplane/label @siteplane/progress @siteplane/select @siteplane/separator @siteplane/switch

Usage

import { Card, CardPanel } from "@/siteplane/components/card";

<Card>
  <CardPanel>Content</CardPanel>
</Card>;

Composition

CardFrame (optional)
├── CardFrameHeader
│   ├── CardFrameTitle
│   ├── CardFrameDescription
│   └── CardFrameAction
├── Card
│   ├── CardHeader
│   │   ├── CardMedia (optional)
│   │   ├── CardTitle
│   │   │   └── CardTitleMeta (optional)
│   │   ├── CardDescription
│   │   └── CardAction
│   ├── Separator (optional)
│   ├── CardPanel (CardContent)
│   ├── Separator (optional)
│   └── CardFooter
├── CardFrameBleed (optional)
└── CardFrameFooter

Header action

Place a link or button in CardAction. Beside a title and a description, it sits at the top right of the header. Beside a title alone, it centers on the title's line and keeps the header as tall as a header without an action: a button taller than the line, up to the xl size, reaches into the header's padding instead of making the header taller. Cards side by side therefore keep one header height, with or without an action.

Recent invoices
Your last three billing periods.
  • INV-1042Sep 1, 2026
    $240.00
  • INV-1031Aug 1, 2026
    $240.00
  • INV-1020Jul 1, 2026
    $180.00
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Card,
  CardAction,
  CardDescription,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";

const invoices = [
  { amount: "$240.00", date: "Sep 1, 2026", id: "INV-1042" },
  { amount: "$240.00", date: "Aug 1, 2026", id: "INV-1031" },
  { amount: "$180.00", date: "Jul 1, 2026", id: "INV-1020" },
];

export default function Example() {
  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Recent invoices</CardTitle>
        <CardDescription>Your last three billing periods.</CardDescription>
        <CardAction>
          <Button render={<a href="#invoices" />} variant="link">
            View all
          </Button>
        </CardAction>
      </CardHeader>
      <CardPanel>
        <ul className="grid gap-3 text-sm">
          {invoices.map((invoice) => (
            <li
              className="flex items-center justify-between gap-4"
              key={invoice.id}
            >
              <div className="grid gap-0.5">
                <span className="font-medium">{invoice.id}</span>
                <span className="text-muted-foreground">{invoice.date}</span>
              </div>
              <span className="tabular-nums">{invoice.amount}</span>
            </li>
          ))}
        </ul>
      </CardPanel>
    </Card>
  );
}

Header media and title context

Put an avatar or an icon in CardMedia as the first part of CardHeader, to show who or what the card is about. It sits before the title and the description, 12 px from them. A bare icon is 20 px in the accent color and centers on the first line of the title; an avatar taller than that line starts at its top. Beside a title alone, the media, the title and the action center on each other.

Set tone="neutral" on CardMedia to draw a bare icon in the secondary text color, --sp-rw-fg-secondary, instead of the accent. A neutral icon suits a module icon that leaves color to states. An avatar or an image keeps its own colors.

<CardMedia tone="neutral">
  <ChartColumnIcon aria-hidden="true" />
</CardMedia>

Put context that belongs to the title, such as a period, a count or a short status, in CardTitleMeta inside CardTitle, after the title's text. It shares the title's line in the quieter text of CardDescription, and moves below the title when the line is full. Rendered as a heading with render={<h2 />}, the title's text and its context form the heading together.

LH
Lea Hartmann submitted 3 changes
2 hours ago

VisitorsLast 7 days

4,812

8% more than the week before.

"use client";

import { ChartColumnIcon } from "lucide-react";
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/siteplane/components/avatar";
import { Button } from "@/siteplane/components/button";
import {
  Card,
  CardAction,
  CardDescription,
  CardHeader,
  CardMedia,
  CardPanel,
  CardTitle,
  CardTitleMeta,
} from "@/siteplane/components/card";

export default function Example() {
  return (
    <div className="grid w-full max-w-md gap-4">
      <Card>
        <CardHeader>
          <CardMedia>
            <Avatar>
              <AvatarImage
                alt="Lea Hartmann"
                src="https://ui.siteplane.io/avatars/lea.webp"
              />
              <AvatarFallback>LH</AvatarFallback>
            </Avatar>
          </CardMedia>
          <CardTitle>Lea Hartmann submitted 3 changes</CardTitle>
          <CardDescription>2 hours ago</CardDescription>
          <CardAction>
            <Button variant="outline">Review</Button>
          </CardAction>
        </CardHeader>
      </Card>
      <Card>
        <CardHeader>
          <CardMedia>
            <ChartColumnIcon aria-hidden="true" />
          </CardMedia>
          <CardTitle render={<h2 />}>
            Visitors
            <CardTitleMeta>Last 7 days</CardTitleMeta>
          </CardTitle>
          <CardAction>
            <Button render={<a href="#analytics" />} variant="link">
              Open analytics
            </Button>
          </CardAction>
        </CardHeader>
        <CardPanel className="grid gap-1">
          <p className="font-semibold text-2xl tabular-nums">4,812</p>
          <p className="text-muted-foreground text-sm">
            8% more than the week before.
          </p>
        </CardPanel>
      </Card>
    </div>
  );
}

Place a Separator after CardHeader or before CardFooter to divide them from the panel. The divided header keeps as much space below its text as above it, 20 px, and the divided footer keeps 16 px above and below its actions. The panel keeps its own padding on the other side of each line. A border-b on CardHeader or a border-t on CardFooter divides them the same way.

Payouts
Sent to the account ending in 4410.
  • PO-2041Oct 14$1,240.00
  • PO-2033Oct 7$980.50
  • PO-2027Sep 30$1,102.25
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Card,
  CardDescription,
  CardFooter,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";
import { Separator } from "@/siteplane/components/separator";

const payouts = [
  { amount: "$1,240.00", date: "Oct 14", id: "PO-2041" },
  { amount: "$980.50", date: "Oct 7", id: "PO-2033" },
  { amount: "$1,102.25", date: "Sep 30", id: "PO-2027" },
];

export default function Example() {
  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Payouts</CardTitle>
        <CardDescription>Sent to the account ending in 4410.</CardDescription>
      </CardHeader>
      <Separator role="presentation" />
      <CardPanel>
        <ul className="grid gap-3 text-sm">
          {payouts.map((payout) => (
            <li
              className="flex items-center justify-between gap-4"
              key={payout.id}
            >
              <span className="grid gap-0.5">
                <span className="font-medium">{payout.id}</span>
                <span className="text-muted-foreground">{payout.date}</span>
              </span>
              <span className="tabular-nums">{payout.amount}</span>
            </li>
          ))}
        </ul>
      </CardPanel>
      <Separator role="presentation" />
      <CardFooter className="justify-between gap-4">
        <span className="text-muted-foreground text-sm">
          Next payout on Oct 21
        </span>
        <Button variant="outline">Statements</Button>
      </CardFooter>
    </Card>
  );
}

Density

Set density="compact" to pad the parts of a card by 16 px instead of 20 px. Without the prop, a card takes the density of the SiteplaneUIProvider around it, so a compact provider makes every card inside it compact, and a card's own density wins over the provider's. The space between the parts stays the same.

<Card density="compact">

Inset

Every part of a card is padded by the card inset, 20 px by default and 16 px in the compact density: the header, the panel and the footer, the sides of the tinted header and of the divided bands, and the space above and below a divided header. The space between the parts stays the same: 12 px below a header, 16 px above a footer and the 16 px of a divided footer.

Three tokens set the inset in place of the density:

  • --sp-rw-card-inset sets all four sides.
  • --sp-rw-card-inset-block sets the space above and below.
  • --sp-rw-card-inset-inline sets the sides.

--sp-rw-card-inset-block and --sp-rw-card-inset-inline take the value of --sp-rw-card-inset until you set them. Set the tokens after the base import to change every card, as Theming shows, or on one card with its className:

<Card className="[--sp-rw-card-inset:--spacing(6)]">

For less space above and below than at the sides, set the block token alone:

<Card className="[--sp-rw-card-inset-block:--spacing(3)]">

The cards joined into a CardFrame take the frame's inset at their sides instead, --card-frame-inset, so their text lines up with the frame's header.

Storage
42 GB of 100 GB used.
x
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Card,
  CardDescription,
  CardFooter,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";
import { Progress } from "@/siteplane/components/progress";

export default function Example() {
  return (
    <Card className="w-full max-w-md [--sp-rw-card-inset:--spacing(6)]">
      <CardHeader>
        <CardTitle>Storage</CardTitle>
        <CardDescription>42 GB of 100 GB used.</CardDescription>
      </CardHeader>
      <CardPanel>
        <Progress aria-label="Storage used" value={42} />
      </CardPanel>
      <CardFooter>
        <Button variant="outline">Manage storage</Button>
      </CardFooter>
    </Card>
  );
}

Joined frame

Use CardFrame for a titled section with a frame-level action, and place Card directly inside it. The frame draws the outer edge and clips the card at it, so the card runs edge to edge with square corners and adds only the hairline where it begins. CardFrameBleed extends custom content, such as a table, to the outer edge. The frame's header and footer, the parts of a joined card and the outer columns of a table inside CardFrameBleed share one horizontal inset.

Projects
Manage your projects
No projects yet
Get started by adding your first project.
"use client";

import { FolderIcon, PlusIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Card,
  CardFrame,
  CardFrameAction,
  CardFrameDescription,
  CardFrameHeader,
  CardFrameTitle,
  CardPanel,
} from "@/siteplane/components/card";
import {
  Empty,
  EmptyDescription,
  EmptyHeader,
  EmptyMedia,
  EmptyTitle,
} from "@/siteplane/components/empty-state";

export default function Example() {
  return (
    <CardFrame className="w-full max-w-md">
      <CardFrameHeader>
        <CardFrameTitle>Projects</CardFrameTitle>
        <CardFrameDescription>Manage your projects</CardFrameDescription>
        <CardFrameAction>
          <Button variant="outline">
            <PlusIcon />
            Add project
          </Button>
        </CardFrameAction>
      </CardFrameHeader>
      <Card>
        <CardPanel>
          <Empty>
            <EmptyHeader>
              <EmptyMedia variant="icon">
                <FolderIcon />
              </EmptyMedia>
              <EmptyTitle>No projects yet</EmptyTitle>
              <EmptyDescription>
                Get started by adding your first project.
              </EmptyDescription>
            </EmptyHeader>
          </Empty>
        </CardPanel>
      </Card>
    </CardFrame>
  );
}

Variants

The variant prop changes only the surface treatment; the parts inside stay the same. The sections below group the eight variants by where a card sits and what it shows.

Raised, header and subtle

raised is the default: the card color with the edge and a soft shadow, for a card on the page. Set variant="header" for the raised Card with a tinted header and a thin divider. Compose the same CardHeader, CardTitle and CardPanel parts; the Card owns the header background, border, spacing and its compact 14 px semibold title in both themes. Set variant="subtle" for a flatter surface in the page tone without a shadow.

<Card variant="header">
  <CardHeader>
    <CardTitle>Views in 24 hours</CardTitle>
  </CardHeader>
  <CardPanel>2,000 views</CardPanel>
</Card>
Storage
42 GB of 100 GB used.
x
Views in 24 hours

2,000

12% more than the day before.

Scheduled reports
Weekly snapshots without manual exports.
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Card,
  CardAction,
  CardDescription,
  CardFooter,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";
import { Progress } from "@/siteplane/components/progress";

export default function Example() {
  return (
    <div className="grid w-full max-w-md gap-4">
      <Card>
        <CardHeader>
          <CardTitle>Storage</CardTitle>
          <CardDescription>42 GB of 100 GB used.</CardDescription>
          <CardAction>
            <Button variant="outline">Upgrade</Button>
          </CardAction>
        </CardHeader>
        <CardPanel>
          <Progress aria-label="Storage used" value={42} />
        </CardPanel>
      </Card>
      <Card variant="header">
        <CardHeader>
          <CardTitle>Views in 24 hours</CardTitle>
        </CardHeader>
        <CardPanel className="grid gap-1">
          <p className="font-semibold text-2xl tabular-nums">2,000</p>
          <p className="text-muted-foreground text-sm">
            12% more than the day before.
          </p>
        </CardPanel>
      </Card>
      <Card variant="subtle">
        <CardHeader>
          <CardTitle>Scheduled reports</CardTitle>
          <CardDescription>
            Weekly snapshots without manual exports.
          </CardDescription>
        </CardHeader>
        <CardFooter>
          <Button className="w-full">Set up reports</Button>
        </CardFooter>
      </Card>
    </div>
  );
}

Inset and elevated

Both are for a surface that sits on a surface of the same color. Set variant="inset" for a surface that sits inside another surface, such as a file row inside a Card. It uses the page tone, is pressed in with a soft inner top shadow and the native hairline, and has no elevation, so it never floats white on white. Set variant="elevated" for a surface that must lift off a surface of the same color, such as a plan to choose on a white page: it keeps the raised fill and adds the hairline with a soft all-round shadow that also shows the top edge, biased slightly downward.

Attachments
Files shared with this project.
Brand-guidelines.pdf2.4 MB
Launch-budget.xlsx180 KB
Starter
For side projects.

$0 / month

Pro
For growing teams.

$20 / month

"use client";

import { Button } from "@/siteplane/components/button";
import {
  Card,
  CardDescription,
  CardFooter,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";
import { FileTypeIcon } from "@/siteplane/components/file-type-icon";

const files = [
  { name: "Brand-guidelines.pdf", size: "2.4 MB" },
  { name: "Launch-budget.xlsx", size: "180 KB" },
];

const plans = [
  { description: "For side projects.", name: "Starter", price: "$0" },
  { description: "For growing teams.", name: "Pro", price: "$20" },
];

export default function Example() {
  return (
    <div className="grid w-full max-w-md gap-6">
      <Card>
        <CardHeader>
          <CardTitle>Attachments</CardTitle>
          <CardDescription>Files shared with this project.</CardDescription>
        </CardHeader>
        <CardPanel className="grid gap-2">
          {files.map((file) => (
            <Card
              className="flex-row items-center gap-3 px-3 py-2"
              key={file.name}
              variant="inset"
            >
              <FileTypeIcon name={file.name} />
              <div className="grid min-w-0 text-sm">
                <span className="truncate font-medium">{file.name}</span>
                <span className="text-muted-foreground">{file.size}</span>
              </div>
            </Card>
          ))}
        </CardPanel>
      </Card>
      <div className="grid grid-cols-2 gap-4">
        {plans.map((plan) => (
          <Card key={plan.name} variant="elevated">
            <CardHeader>
              <CardTitle>{plan.name}</CardTitle>
              <CardDescription>{plan.description}</CardDescription>
            </CardHeader>
            <CardPanel>
              <p className="font-semibold text-2xl tabular-nums">
                {plan.price}
                <span className="font-normal text-muted-foreground text-sm">
                  {" "}
                  / month
                </span>
              </p>
            </CardPanel>
            <CardFooter>
              <Button className="w-full" variant="outline">
                Choose {plan.name}
              </Button>
            </CardFooter>
          </Card>
        ))}
      </div>
    </div>
  );
}

Message and error

Set variant="message" for a bordered message surface without elevation. It uses the normal 12 px surface radius with a smaller 4 px top-start corner (top-left in left-to-right layouts). Set variant="error" for the raised Card of an item that failed, such as an upload that did not finish. The Card adds the destructive contour of an invalid field and a faint tint. State the failure in text or with an icon as well; the color never carries it alone.

BC
The new layout looks great. Can we ship it on Friday?
Ben Carter, 9:41 AM

logo-dark.png did not upload

The file is larger than the 5 MB limit.

"use client";

import { CircleAlertIcon } from "lucide-react";
import { Avatar, AvatarFallback } from "@/siteplane/components/avatar";
import { Button } from "@/siteplane/components/button";
import { Card } from "@/siteplane/components/card";

export default function Example() {
  return (
    <div className="grid w-full max-w-md gap-4">
      <div className="flex items-start gap-3">
        <Avatar>
          <AvatarFallback>BC</AvatarFallback>
        </Avatar>
        <div className="grid min-w-0 gap-1">
          <Card className="p-3 text-sm" variant="message">
            The new layout looks great. Can we ship it on Friday?
          </Card>
          <span className="text-muted-foreground text-xs">
            Ben Carter, 9:41 AM
          </span>
        </div>
      </div>
      <Card className="flex-row items-start gap-3 p-4 text-sm" variant="error">
        <CircleAlertIcon className="size-4 h-lh shrink-0 text-destructive" />
        <div className="grid min-w-0 flex-1 gap-0.5">
          <p className="font-medium">logo-dark.png did not upload</p>
          <p className="text-muted-foreground">
            The file is larger than the 5 MB limit.
          </p>
        </div>
        <Button size="sm" variant="outline">
          Try again
        </Button>
      </Card>
    </div>
  );
}

Glass

Set variant="glass" for a card that sits on a photo, a video or a color surface. In place of the card color, edge and shadow, the card shows a tinted, blurred view of what lies behind it, with a light rim. Its parts and spacing stay the same. Glass shows every component with a glass variant.

The gradient only makes the glass visible. It is not part of the component.

Notifications
Choose what reaches you.
"use client";

import {
  Card,
  CardDescription,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";
import { Label } from "@/siteplane/components/label";
import { Switch } from "@/siteplane/components/switch";

export default function Example() {
  return (
    <Card className="w-full max-w-md" variant="glass">
      <CardHeader>
        <CardTitle>Notifications</CardTitle>
        <CardDescription>Choose what reaches you.</CardDescription>
      </CardHeader>
      <CardPanel className="grid gap-3">
        <Label className="w-full cursor-pointer justify-between">
          Live presence
          <Switch defaultChecked />
        </Label>
        <Label className="w-full cursor-pointer justify-between">
          Weekly digest
          <Switch />
        </Label>
      </CardPanel>
    </Card>
  );
}

Edge and background

A raised card draws one edge: a hairline contour with a soft shadow, strong enough to read on a white page and on another card. Cards, frames and panels share it, so each boundary has a single hairline. The header and error cards keep this edge, and inset and elevated cards draw the same hairline with a shadow of their own. A subtle card has no edge, a message card draws a border in its place, and a glass card shows a light rim. No prop or provider option changes the strength of the edge.

On the page canvas

Views
Last 24 hours

2,000

On the card color

Views
Last 24 hours

2,000

"use client";

import {
  Card,
  CardDescription,
  CardHeader,
  CardPanel,
  CardTitle,
} from "@/siteplane/components/card";

function ViewsCard() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Views</CardTitle>
        <CardDescription>Last 24 hours</CardDescription>
      </CardHeader>
      <CardPanel>
        <p className="font-semibold text-2xl tabular-nums">2,000</p>
      </CardPanel>
    </Card>
  );
}

export default function Example() {
  return (
    <div className="grid w-full max-w-md grid-cols-2 gap-4">
      <div className="grid gap-3 rounded-2xl bg-background p-4">
        <p className="text-muted-foreground text-xs">On the page canvas</p>
        <ViewsCard />
      </div>
      <div className="grid gap-3 rounded-2xl bg-[var(--sp-rw-bg-secondary)] p-4">
        <p className="text-muted-foreground text-xs">On the card color</p>
        <ViewsCard />
      </div>
    </div>
  );
}

Cards read best on a background a little darker than the card, such as the page canvas (bg-background): the edge and the soft shadow then separate the card clearly, especially on high-density displays.

API reference

Component properties
PropTypeDefaultDescription
Card.variantraised | subtle | header | message | error | inset | elevated | glassraisedSurface treatment; header adds a divided header, message adds a contour and a smaller top-start corner, error adds a destructive contour and tint, inset presses a nested surface into its parent, elevated lifts it with an all-round shadow, glass shows a tinted, blurred view of what lies behind the card.
Card.densitydefault | compactprovider / defaultInset of the parts: 20 px, or 16 px compact. The inset tokens below take its place.
--sp-rw-card-insetCSS lengthunset: 20 px, 16 px compactPadding of the header, panel and footer, the sides of the bands and the space above and below a divided header. Set it on :root after the base import, or on one card with className="[--sp-rw-card-inset:--spacing(6)]".
--sp-rw-card-inset-blockCSS length--sp-rw-card-insetThe space above and below: the padding of the header, panel and footer and the space above and below a divided header.
--sp-rw-card-inset-inlineCSS length--sp-rw-card-insetThe sides of the header, panel and footer and of the bands. The cards joined into a CardFrame take --card-frame-inset instead.
CardFrame --card-frame-insetCSS length1.5remHorizontal inset of CardFrameHeader, CardFrameFooter and the outer columns of a table inside CardFrameBleed. Set it on CardFrame, for example with className="[--card-frame-inset:--spacing(4)]".
CardMediadiv props—Leading avatar or icon of CardHeader, before the title and description. A bare icon is 20 px in the accent color.
CardMedia.toneaccent | neutralaccentColor of a bare icon: the accent, or the secondary text color, --sp-rw-fg-secondary.
CardTitleMetaspan props—Context inside CardTitle, after its text, in the quieter description text. Moves below the title when the line is full.
All parts.renderReactElement | render functiondivCompose native semantics onto each part.
All parts.classNamestringunsetAdditional classes.
All parts.childrenReactNodeunsetContent.

All parts accept native div attributes and refs through Base UI composition; CardTitleMeta accepts native span attributes. CardHeader lays out CardMedia, CardTitle, CardDescription and CardAction; CardPanel supplies flexible padded content; CardContent is the exported alias for CardPanel; CardFooter holds lower content. CardFrame, CardFrameHeader, CardFrameTitle, CardFrameDescription, CardFrameAction, CardFrameFooter and CardFrameBleed supply the corresponding outer composition. Only Card owns variant and density. No part adds data loading, selection, click behavior or focus handling.

Read the installed source.

Usage notes

Title parts render divs by default. Use render={<h2 />} if the title belongs in the document outline. Keep interactive controls separate instead of turning a card containing buttons into another nested button.

A long title wraps. To keep it on one line, put a Truncate inside CardTitle. The title then ends in an ellipsis before the CardAction and shows in full in a tooltip on hover. A Truncate with lines={2} inside CardDescription keeps the first two lines of a description.

Tables inside a frame

Tables inside a direct child Card or CardFrameBleed align their outer columns with the frame header and footer. Set --card-frame-inset on CardFrame to adjust that shared inset; a table-specific --table-edge-padding can override it.