A container that groups related content under an optional heading. Compose it with media, an overlay, header, meta, actions, body, a footer and a coloured band for destination, profile, listing and detail-panel cards.
import { Card } from "@rdloom/react";
export default function CardBasicExample() {
return (
<div className="w-80 max-w-full">
<Card title="Team plan" description="Up to 10 members">
<p>Shared projects, version history and priority support.</p>
</Card>
</div>
);
}Installation
npx rdloom add cardCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/card.jsonWorks, but without upgrade tracking.
Usage
import { Card } from "@rdloom/react";
<div className="w-80 max-w-full">
<Card title="Team plan" description="Up to 10 members">
<p>Shared projects, version history and priority support.</p>
</Card>
</div>With footer
import { Button, Card } from "@rdloom/react";
export default function CardWithFooterExample() {
return (
<div className="w-80 max-w-full">
<Card
title="Delete project"
description="This can't be undone."
footer={
<>
<Button variant="danger" size="sm">
Delete
</Button>
<Button variant="ghost" size="sm">
Cancel
</Button>
</>
}
>
<p>All files and comments in "Website redesign" will be removed for everyone.</p>
</Card>
</div>
);
}Variants
import { Card } from "@rdloom/react";
export default function CardVariantsExample() {
return (
<div className="grid w-[40rem] max-w-full gap-4 sm:grid-cols-3">
<Card title="Outlined" variant="outlined">
A border, no shadow.
</Card>
<Card title="Raised" variant="raised">
A border and a shadow.
</Card>
<Card title="Subtle" variant="subtle">
A tinted block.
</Card>
</div>
);
}Image top
San Francisco
Premium economy
- from $240
- SFO
import { Card, CardDescription, CardHeader, CardMedia, CardMeta, CardMetaItem, CardTitle, PlaneIcon, TagIcon } from "@rdloom/react";
// Placeholder art drawn with gradients. Swap CardMedia's style for src="..." to use a photo.
const bridge =
"radial-gradient(circle at 78% 22%, #fde68a 0 7%, transparent 8%), linear-gradient(to bottom, #fdba74 0%, #fecaca 38%, #7dd3fc 39%, #0369a1 100%)";
export default function CardImageTopExample() {
return (
<div className="w-80 max-w-full">
<Card variant="floating" rounded="large" padding="sm">
<CardMedia alt="A bridge at sunset over the bay" aspectRatio="photo" style={{ background: bridge }} />
<CardHeader>
<CardTitle size="lg">San Francisco</CardTitle>
<CardDescription>Premium economy</CardDescription>
<CardMeta className="mt-2">
<CardMetaItem icon={<TagIcon />}>from $240</CardMetaItem>
<CardMetaItem icon={<PlaneIcon />}>SFO</CardMetaItem>
</CardMeta>
</CardHeader>
</Card>
</div>
);
}Full image
New York
Economy
- from $120
- JFK
import { Button, Card, CardDescription, CardIconButton, CardMedia, CardMeta, CardMetaItem, CardOverlay, CardTitle, HeartIcon, PlaneIcon, TagIcon } from "@rdloom/react";
// Placeholder art drawn with gradients. Swap CardMedia's style for src="..." to use a photo.
const skyline =
"linear-gradient(to top, #1e293b, transparent 40%), linear-gradient(90deg, transparent 0 12%, #334155 12% 22%, transparent 22% 30%, #475569 30% 44%, transparent 44% 52%, #334155 52% 60%, transparent 60%), linear-gradient(to bottom, #38bdf8 0%, #bae6fd 55%, #fde68a 100%)";
export default function CardFullImageExample() {
return (
<div className="w-80 max-w-full">
<Card layout="overlay" rounded="large" variant="floating" className="min-h-[26rem]">
<CardMedia alt="A city skyline at dusk" style={{ background: skyline }} />
<CardOverlay tone="dark">
<div className="flex flex-col gap-0.5">
<CardTitle size="lg">New York</CardTitle>
<CardDescription>Economy</CardDescription>
</div>
<CardMeta>
<CardMetaItem icon={<TagIcon />}>from $120</CardMetaItem>
<CardMetaItem icon={<PlaneIcon />}>JFK</CardMetaItem>
</CardMeta>
<Button variant="secondary" className="w-full !rounded-full">
Search flight
</Button>
</CardOverlay>
<CardIconButton label="Save New York" tone="frosted" className="absolute end-5 top-5 z-20">
<HeartIcon className="size-5" />
</CardIconButton>
</Card>
</div>
);
}With actions
San Francisco
Premium economy
- from $240
- SFO
import {
Button,
Card,
CardActions,
CardDescription,
CardHeader,
CardIconButton,
CardMedia,
CardMeta,
CardMetaItem,
CardTitle,
HeartIcon,
PlaneIcon,
TagIcon,
} from "@rdloom/react";
// Placeholder art drawn with gradients. Swap CardMedia's style for src="..." to use a photo.
const bridge =
"radial-gradient(circle at 78% 22%, #fde68a 0 7%, transparent 8%), linear-gradient(to bottom, #fdba74 0%, #fecaca 38%, #7dd3fc 39%, #0369a1 100%)";
export default function CardWithActionsExample() {
return (
<div className="w-80 max-w-full">
<Card variant="floating" rounded="large" padding="sm">
<CardMedia alt="A bridge at sunset over the bay" style={{ background: bridge }} />
<CardHeader>
<CardTitle size="lg">San Francisco</CardTitle>
<CardDescription>Premium economy</CardDescription>
<CardMeta className="mt-2">
<CardMetaItem icon={<TagIcon />}>from $240</CardMetaItem>
<CardMetaItem icon={<PlaneIcon />}>SFO</CardMetaItem>
</CardMeta>
</CardHeader>
<CardActions>
<Button className="flex-1 !rounded-full">Search flight</Button>
<CardIconButton label="Save San Francisco" tone="outlined">
<HeartIcon filled className="size-5 text-[var(--rd-color-feedback-danger)]" />
</CardIconButton>
</CardActions>
</Card>
</div>
);
}Profile
Amelia OrtizVerified
Product designer. Writes about calm interfaces and the small details that make them feel kind.
- 312
- 48
import { Button, Card, CardDescription, CardFooter, CardHeader, CardMedia, CardMeta, CardMetaItem, CardTitle, CommentIcon, UserIcon, VerifiedIcon } from "@rdloom/react";
// Placeholder art drawn with gradients. Swap CardMedia's style for src="..." to use a photo.
const portrait =
"radial-gradient(circle at 50% 38%, #fcd5b5 0 16%, transparent 17%), radial-gradient(ellipse at 50% 100%, #475569 0 38%, transparent 39%), linear-gradient(to bottom, #c7d2fe, #fbcfe8)";
export default function CardProfileExample() {
return (
<div className="w-80 max-w-full">
<Card variant="floating" rounded="large" padding="sm">
<CardMedia alt="Portrait of Amelia Ortiz" aspectRatio="square" style={{ background: portrait }} />
<CardHeader>
<CardTitle>
Amelia Ortiz
<VerifiedIcon className="size-4 shrink-0 text-[var(--rd-color-feedback-success)]" />
<span className="sr-only">Verified</span>
</CardTitle>
<CardDescription>Product designer. Writes about calm interfaces and the small details that make them feel kind.</CardDescription>
</CardHeader>
<CardFooter className="justify-between">
<CardMeta>
<CardMetaItem icon={<UserIcon />}>312</CardMetaItem>
<CardMetaItem icon={<CommentIcon />}>48</CardMetaItem>
</CardMeta>
<Button size="sm" className="!rounded-full">
Follow +
</Button>
</CardFooter>
</Card>
</div>
);
}Profile full image
Amelia OrtizVerified
Product designer. Writes about calm interfaces and the small details that make them feel kind.
- 312
- 48
import { Button, Card, CardDescription, CardMedia, CardMeta, CardMetaItem, CardOverlay, CardTitle, CommentIcon, UserIcon, VerifiedIcon } from "@rdloom/react";
// Placeholder art drawn with gradients. Swap CardMedia's style for src="..." to use a photo.
const portrait =
"radial-gradient(circle at 50% 34%, #fcd5b5 0 13%, transparent 14%), radial-gradient(ellipse at 50% 70%, #475569 0 30%, transparent 31%), linear-gradient(to bottom, #c7d2fe, #fbcfe8)";
export default function CardProfileFullImageExample() {
return (
<div className="w-80 max-w-full">
<Card layout="overlay" rounded="large" variant="floating" className="min-h-[26rem]">
<CardMedia alt="Portrait of Amelia Ortiz" style={{ background: portrait }} />
<CardOverlay tone="light">
<div className="flex flex-col gap-0.5">
<CardTitle>
Amelia Ortiz
<VerifiedIcon className="size-4 shrink-0 text-[var(--rd-color-feedback-success)]" />
<span className="sr-only">Verified</span>
</CardTitle>
<CardDescription>Product designer. Writes about calm interfaces and the small details that make them feel kind.</CardDescription>
</div>
<div className="flex items-center justify-between gap-2">
<CardMeta>
<CardMetaItem icon={<UserIcon />}>312</CardMetaItem>
<CardMetaItem icon={<CommentIcon />}>48</CardMetaItem>
</CardMeta>
<Button size="sm" className="!rounded-full">
Follow +
</Button>
</div>
</CardOverlay>
</Card>
</div>
);
}Job listing
Senior product designer
$3,500-5,500 net
- FULL TIME
- REMOTE
Frontend engineer
$4,000-6,000 net
- FULL TIME
- HYBRID
import { Card, CardBand, CardBody, CardDescription, CardMeta, CardTitle } from "@rdloom/react";
function Chip({ children }: { children: string }) {
return (
<li className="rounded-full border border-[var(--rd-color-border-default)] px-2.5 py-0.5 text-xs font-medium tracking-wide text-[var(--rd-color-text-default)]">
{children}
</li>
);
}
// A made-up mark: two overlapping shapes.
function Mark() {
return (
<span aria-hidden="true" className="relative block size-11 overflow-hidden rounded-xl bg-[var(--rd-color-text-default)]">
<span className="absolute -start-1 top-2 size-6 rounded-full bg-[var(--rd-color-surface-default)]" />
<span className="absolute bottom-1.5 end-1.5 size-4 rotate-45 rounded-sm bg-[var(--rd-color-action-primary)]" />
</span>
);
}
function Listing({ title, salary, tags, posted, tone }: { title: string; salary: string; tags: string[]; posted: string; tone: "peach" | "lilac" | "rose" }) {
return (
<Card padding="none" rounded="large" variant="floating">
<CardBody>
<Mark />
<div className="flex flex-col gap-0.5">
<CardTitle>{title}</CardTitle>
<CardDescription>{salary}</CardDescription>
</div>
<CardMeta aria-label="Job details" className="gap-2">
{tags.map((t) => (
<Chip key={t}>{t}</Chip>
))}
</CardMeta>
</CardBody>
<CardBand tone={tone}>{posted}</CardBand>
</Card>
);
}
export default function CardJobListingExample() {
return (
<div className="grid w-[40rem] max-w-full gap-4 sm:grid-cols-2">
<Listing title="Senior product designer" salary="$3,500-5,500 net" tags={["FULL TIME", "REMOTE"]} posted="Posted 2 days ago" tone="peach" />
<Listing title="Frontend engineer" salary="$4,000-6,000 net" tags={["FULL TIME", "HYBRID"]} posted="Posted 5 days ago" tone="lilac" />
</div>
);
}Detail panel
Brand identity review
Thu, Mar 14 · 10:00 to 11:00
- PriorityHigh
- Attendees8 people
import {
AvatarGroup,
Badge,
BellIcon,
Button,
CalendarIcon,
Card,
CardActions,
CardDescription,
CardFooter,
CardHeader,
CardIconButton,
CardTitle,
ChevronRightIcon,
CloseIcon,
PinIcon,
StarIcon,
UsersIcon,
} from "@rdloom/react";
import type { ReactNode } from "react";
function Row({ icon, label, children, highlighted }: { icon: ReactNode; label: string; children: ReactNode; highlighted?: boolean }) {
return (
<li
className={
"flex items-center gap-3 rounded-[var(--rd-radius-control)] px-3 py-2 " +
(highlighted ? "bg-[var(--rd-color-surface-selected)]" : "")
}
>
<span className="text-[var(--rd-color-text-muted)]">{icon}</span>
<span className="text-[var(--rd-color-text-muted)]">{label}</span>
<span className="ms-auto flex items-center gap-2 font-medium">{children}</span>
</li>
);
}
export default function CardDetailPanelExample() {
return (
<div className="w-[28rem] max-w-full">
<Card variant="floating" rounded="large" padding="sm">
<CardHeader className="gap-3 rounded-[calc(var(--rd-radius-media)-0.5rem)] bg-[var(--rd-color-surface-subtle)] p-4">
<div className="flex flex-wrap items-start gap-3">
<span aria-hidden="true" className="grid size-11 shrink-0 place-items-center rounded-xl bg-[var(--rd-color-action-primary)] text-[var(--rd-color-action-on-primary)]">
<CalendarIcon className="size-5" />
</span>
<div className="flex min-w-40 flex-1 flex-col">
<CardTitle>Brand identity review</CardTitle>
<CardDescription>Thu, Mar 14 · 10:00 to 11:00</CardDescription>
</div>
<CardActions className="ms-auto gap-1">
<CardIconButton label="Turn on reminders" tone="ghost" className="size-8">
<BellIcon />
</CardIconButton>
<CardIconButton label="Pin to top" tone="ghost" className="size-8">
<PinIcon />
</CardIconButton>
<CardIconButton label="Close details" tone="ghost" className="size-8">
<CloseIcon className="size-4" />
</CardIconButton>
</CardActions>
</div>
<div className="flex items-center gap-2">
<AvatarGroup
size="sm"
label="Marketing team"
avatars={[{ name: "Ada Lovelace" }, { name: "Grace Hopper" }, { name: "Katherine Johnson" }]}
/>
<span className="text-[var(--rd-color-text-muted)]">with Marketing team</span>
</div>
</CardHeader>
<ul className="m-0 flex list-none flex-col gap-1 p-0">
<Row icon={<StarIcon />} label="Priority">
<Badge variant="warning">
<StarIcon filled className="size-3" />
High
</Badge>
</Row>
<Row icon={<UsersIcon />} label="Attendees" highlighted>
8 people
<CardIconButton label="Show attendees" tone="ghost" className="size-8">
<ChevronRightIcon />
</CardIconButton>
</Row>
</ul>
<CardFooter className="justify-end">
<Button variant="secondary" size="sm">
Skip
</Button>
<Button variant="secondary" size="sm">
Reschedule
</Button>
<Button size="sm">Join</Button>
</CardFooter>
</Card>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
childrenrequiredThe card's content. | node | none |
titleHeading shown at the top. Also names the card for screen readers. | string | none |
descriptionSupporting text under the title. | string | none |
footerActions or meta information along the bottom, e.g. buttons. | node | none |
headingLevelHeading level for the title, 2 to 6. Pick the level that fits where the card sits in the page's outline. | number | 3 |
variantoutlined has a border, raised adds a small shadow, subtle is a tinted block with no border, floating is a borderless card with a large soft shadow. | "outlined" | "raised" | "subtle" | "floating" | "outlined" |
paddingInner spacing. none leaves the edges to the parts inside (full-bleed media, a band). | "none" | "sm" | "md" | "lg" | "md" |
roundedCorner radius. large is the 24 px media-card radius; parts inside follow it. | "default" | "large" | "default" |
layoutstack lays the parts out in a column. overlay makes a CardMedia fill the whole card, inside a thin frame, with CardOverlay content laid over its bottom. | "stack" | "overlay" | "stack" |
Accessibility
Role group, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
Not interactive.
Screen readers announce
- Entering a titled card announces its name and that it is a group
- The title is reachable as a heading when navigating by headings
- A picture is announced by its alt text; decorative pictures are skipped
- Meta is announced as a list with the number of items
- Icon buttons are announced by their label, e.g. "Save New York"
What your code must do
- With a title, the card is a group labelled by it; without one it is a plain container
- The title (title prop or CardTitle) is a real heading at the level you choose, so the page outline stays correct
- A card is not a landmark and not focusable: put links and buttons inside it
- CardMedia needs alt text describing the picture, or decorative for a picture that adds nothing; an empty alt is only allowed with decorative, never by accident
- Art drawn with children (no src) is exposed as an image named by alt, or hidden when decorative
- Text over an overlay card sits on a scrim: the dark scrim is at least 75 percent of the overlay backdrop colour behind the text zone, so white text keeps 4.5:1 over any picture; the light scrim fades to the solid card surface so default text keeps its normal contrast
- CardIconButton requires an accessible name (label) because the icon is decorative; it shows a focus ring in every tone, including over a picture
- Meta items are a list; their icons are decorative, so the text must carry the meaning ("from $120", not just a tag icon)
- Colour is never the only carrier: the band says what it means in words (POSTED 2 DAYS AGO)
Guidelines
Use it when
- Grouping a small set of related facts or controls, like a plan, a project or a setting
- A dashboard of independent blocks
- Destination, product or profile tiles with a picture
- A listing with a short summary and one action
- A detail panel with a header, rows and actions
Avoid it when
- Rows of the same record type: use a table or data grid
- Wrapping a whole page
Don't
- Cards inside cards
- A heading level that skips (h1 then h4)
- Making the whole card clickable and also putting buttons in it
- Putting long paragraphs over a picture
- An icon button without a label
- Using alt text like "image" or repeating the title word for word
- Dark text over a dark scrim, or white text over the light overlay
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-action-primary--rd-color-border-default--rd-color-chart-4--rd-color-chart-6--rd-color-feedback-success--rd-color-overlay-backdrop--rd-color-surface-default--rd-color-surface-raised--rd-color-surface-subtle--rd-color-text-default--rd-color-text-muted--rd-elevation-floating--rd-radius-media--rd-radius-overlay