Skip to content

Card

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.

Layoutv0.1.0experimentalWCAG 2.2 AAView spec

Team plan

Up to 10 members

Shared projects, version history and priority support.

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 card

Copies 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.json

Works, 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>

Delete project

This can't be undone.

All files and comments in "Website redesign" will be removed for everyone.

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 &quot;Website redesign&quot; will be removed for everyone.</p>
      </Card>
    </div>
  );
}

Variants

Outlined

A border, no shadow.

Raised

A border and a shadow.

Subtle

A tinted block.
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
Posted 2 days ago

Frontend engineer

$4,000-6,000 net

  • FULL TIME
  • HYBRID
Posted 5 days ago
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

with Marketing team
  • 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.

PropTypeDefault
childrenrequired

The card's content.

nodenone
title

Heading shown at the top. Also names the card for screen readers.

stringnone
description

Supporting text under the title.

stringnone
footer

Actions or meta information along the bottom, e.g. buttons.

nodenone
headingLevel

Heading level for the title, 2 to 6. Pick the level that fits where the card sits in the page's outline.

number3
variant

outlined 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"
padding

Inner spacing. none leaves the edges to the parts inside (full-bleed media, a band).

"none" | "sm" | "md" | "lg""md"
rounded

Corner radius. large is the 24 px media-card radius; parts inside follow it.

"default" | "large""default"
layout

stack 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