# Card

Group related content and actions on a surface.

Create project card

[Complete TSX file](/examples/card/default.tsx)

```tsx
"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

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/card
```

With npm: `npx shadcn@4.18.0 add @siteplane/card`. [Registry JSON](/registry/card.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
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

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

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

## Composition

```text
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 with a view-all link

[Complete TSX file](/examples/card/action.tsx)

```tsx
"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.

```tsx
<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.

Cards with an avatar, an icon and title context

[Complete TSX file](/examples/card/header-media.tsx)

```tsx
"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>
  );
}
```

## Divided header and footer

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 with a divided header and footer

[Complete TSX file](/examples/card/divided.tsx)

```tsx
"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](/docs/customization/providers/) 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.

```tsx
<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](/docs/customization/theming/#shared-dimensions) shows, or on one card with its `className`:

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

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

```tsx
<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.

Card with a 24 px inset

[Complete TSX file](/examples/card/inset.tsx)

```tsx
"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](/docs/components/table/#card-composition), 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.

Framed card with a header action

[Complete TSX file](/examples/card/frame.tsx)

```tsx
"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.

```tsx
<Card variant="header">
  <CardHeader>
    <CardTitle>Views in 24 hours</CardTitle>
  </CardHeader>
  <CardPanel>2,000 views</CardPanel>
</Card>
```

Raised, header and subtle cards

[Complete TSX file](/examples/card/variants.tsx)

```tsx
"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.

Inset files in a card and elevated plans

[Complete TSX file](/examples/card/nested.tsx)

```tsx
"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.

Message and error cards

[Complete TSX file](/examples/card/feedback.tsx)

```tsx
"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](/docs/components/glass/) shows every component with a glass variant.

Glass card

[Complete TSX file](/examples/card/glass.tsx)

```tsx
"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.

A card on the page canvas and on the card color

[Complete TSX file](/examples/card/background.tsx)

```tsx
"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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| Card.variant | raised \| subtle \| header \| message \| error \| inset \| elevated \| glass | raised | Surface 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.density | default \| compact | provider / default | Inset of the parts: 20 px, or 16 px compact. The inset tokens below take its place. |
| --sp-rw-card-inset | CSS length | unset: 20 px, 16 px compact | Padding 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-block | CSS length | --sp-rw-card-inset | The 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-inline | CSS length | --sp-rw-card-inset | The 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-inset | CSS length | 1.5rem | Horizontal 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)]". |
| CardMedia | div props | — | Leading avatar or icon of CardHeader, before the title and description. A bare icon is 20 px in the accent color. |
| CardMedia.tone | accent \| neutral | accent | Color of a bare icon: the accent, or the secondary text color, --sp-rw-fg-secondary. |
| CardTitleMeta | span props | — | Context inside CardTitle, after its text, in the quieter description text. Moves below the title when the line is full. |
| All parts.render | ReactElement \| render function | div | Compose native semantics onto each part. |
| All parts.className | string | unset | Additional classes. |
| All parts.children | ReactNode | unset | Content. |
All parts accept native div attributes and refs through [Base UI
composition](https://base-ui.com/react/handbook/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](/source/card).

## 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](/docs/components/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.
