Skip to main content

Card

Moleculebeta@macaw-ds/ui · since 0.1.0

A bordered surface that groups related content: a KPI, a form section, a record summary. Not a page, not a modal.

Import

import { Card, CardHeader, CardTitle, CardDescription, CardBody, CardFooter, cardVariants } from '@macaw-ds/ui';

Props

PropTypeDefaultDescription
padding"none" | "sm" | "md"md

Inner padding for the sections. none when the body is a table or an image that should touch the edges.

elevation"flat" | "raised"flat

flat relies on the border; raised adds a subtle shadow for cards that float over the canvas.

interactivebooleanfalse

Hover border and the focus ring. Use with asChild so the card is an anchor or a button.

Accessibility: An interactive card must be an anchor or a button (asChild) so it has a role and keyboard activation.

asChildbooleanfalse

Render the child element as the card.

childrenReactNode—

Card.Header, Card.Body, Card.Footer in that order.

Slots

Named parts you pass content into.

  • Header
  • Title
  • Description
  • Body
  • Footer

Tokens used

Every value this component renders comes from one of these tokens.

TokenLightDark
card.radius

component · dimension

6px
6px
card.bg.default

component · color

oklch(1 0 0)
oklch(0.15 0.012 250)
card.line

component · color

oklch(0.925 0.005 250)
oklch(0.29 0.012 250)
card.shadow.flat

component · shadow

0px 0px 0px 0px oklch(0 0 0 / 0)
0px 0px 0px 0px oklch(0 0 0 / 0)
card.shadow.raised

component · shadow

0px 1px 2px 0px oklch(0.15 0.012 250 / 0.06), 0px 2px 6px -1px oklch(0.15 0.012 250 / 0.06)
0px 1px 2px 0px oklch(0.15 0.012 250 / 0.06), 0px 2px 6px -1px oklch(0.15 0.012 250 / 0.06)
color.line.subtle

semantic · color

oklch(0.965 0.003 250)
oklch(0.21 0.012 250)
color.line.strong

semantic · color

oklch(0.53 0.012 250)
oklch(0.53 0.012 250)
color.surface.layer-2

semantic · color

oklch(0.985 0.002 250)
oklch(0.21 0.012 250)
color.focus.ring

semantic · color

oklch(0.51 0.2 262)
oklch(0.68 0.16 262)
layout.card-x

semantic · dimension

20px
20px
layout.card-y

semantic · dimension

16px
16px
layout.card-x-sm

semantic · dimension

16px
16px
layout.card-y-sm

semantic · dimension

12px
12px

Eject

Ejecting copies the source into your repo and records it in .macaw/ejected.json, so the fork stays visible. The same file is served as a shadcn-compatible registry item.

With the MACAW CLI
npx @macaw-ds/cli add card --eject
With shadcn
npx shadcn@latest add https://macaw-design-system.vercel.app/r/card.json

Bundle budget: 3,000 bytes gzipped, enforced by size-limit in CI.

Manifest

registry/manifests/card.json, the contract the playground, CLI, plugin and MCP server read.

{
  "name": "Card",
  "slug": "card",
  "package": "@macaw-ds/ui",
  "status": "beta",
  "since": "0.1.0",
  "level": "molecule",
  "category": "layout",
  "purpose": "A bordered surface that groups related content: a KPI, a form section, a record summary. Not a page, not a modal.",
  "description": "A compound component. `Card` is the surface; `Card.Header`, `Card.Title`, `Card.Description`, `Card.Body` and `Card.Footer` lay out its parts with the padding the Card sets. Structure comes from a 1 px border; elevation is optional and subtle. `interactive` with `asChild` turns the whole card into a link or button with the shared focus ring. Padding follows the density mode.",
  "exports": [
    "Card",
    "CardHeader",
    "CardTitle",
    "CardDescription",
    "CardBody",
    "CardFooter",
    "cardVariants"
  ],
  "props": {
    "padding": {
      "type": "enum",
      "values": [
        "none",
        "sm",
        "md"
      ],
      "default": "md",
      "required": false,
      "description": "Inner padding for the sections. none when the body is a table or an image that should touch the edges."
    },
    "elevation": {
      "type": "enum",
      "values": [
        "flat",
        "raised"
      ],
      "default": "flat",
      "required": false,
      "description": "flat relies on the border; raised adds a subtle shadow for cards that float over the canvas."
    },
    "interactive": {
      "type": "boolean",
      "default": false,
      "required": false,
      "description": "Hover border and the focus ring. Use with asChild so the card is an anchor or a button.",
      "a11y": "An interactive card must be an anchor or a button (asChild) so it has a role and keyboard activation."
    },
    "asChild": {
      "type": "boolean",
      "default": false,
      "required": false,
      "description": "Render the child element as the card.",
      "control": "none"
    },
    "children": {
      "type": "node",
      "required": false,
      "description": "Card.Header, Card.Body, Card.Footer in that order.",
      "control": "none"
    }
  },
  "slots": [
    "Header",
    "Title",
    "Description",
    "Body",
    "Footer"
  ],
  "tokens": [
    "card.radius",
    "card.bg.default",
    "card.line",
    "card.shadow.flat",
    "card.shadow.raised",
    "color.line.subtle",
    "color.line.strong",
    "color.surface.layer-2",
    "color.focus.ring",
    "layout.card-x",
    "layout.card-y",
    "layout.card-x-sm",
    "layout.card-y-sm"
  ],
  "a11y": {
    "role": "none for a static card; link or button when interactive with asChild",
    "focusable": {
      "whenProp": "interactive"
    },
    "keyboard": [
      "Static cards are not focusable",
      "Interactive cards (asChild anchor or button): Tab to focus, Enter (and Space for buttons) to activate"
    ],
    "name": "Static cards have none. Interactive cards take the name from their text content; keep the title first so it is read first.",
    "notes": [
      "Card.Title renders an h3 by default. Set `as` to keep heading levels in order on the page.",
      "Do not nest interactive elements inside an interactive card; a card that is a link cannot contain buttons.",
      "The footer's sunken background keeps 4.5:1 for primary and secondary text."
    ]
  },
  "motion": {
    "description": "An interactive card eases its border colour on hover.",
    "tokens": [
      "duration.fast",
      "ease.standard"
    ],
    "whenProp": "interactive"
  },
  "budget": {
    "gzip": 3000
  },
  "rules": [
    "Use the sections (Header, Body, Footer) rather than custom padding; the Card owns its spacing.",
    "Card.Title sets the heading level with `as` to fit the page outline.",
    "Interactive cards use asChild with an anchor or a button and contain no other interactive elements.",
    "One primary action per card footer at most.",
    "From a React Server Component, import the named parts (CardHeader, CardBody, CardFooter); the dotted form (Card.Header) only works inside client components."
  ],
  "dos": [
    "Use padding=\"none\" and put a table in Card.Body so rows run edge to edge.",
    "Use Card.Description for a one-line summary under the title."
  ],
  "donts": [
    "Do not nest cards.",
    "Do not use elevation to signal importance; use position and heading level."
  ],
  "examples": [
    {
      "name": "Default",
      "props": {}
    },
    {
      "name": "Raised",
      "props": {
        "elevation": "raised"
      }
    },
    {
      "name": "Compact",
      "props": {
        "padding": "sm"
      }
    },
    {
      "name": "Interactive",
      "description": "The whole card is a link.",
      "props": {
        "interactive": true
      }
    }
  ],
  "registryDependencies": [
    "text"
  ],
  "dependencies": [
    "radix-ui",
    "class-variance-authority"
  ],
  "related": [
    "text",
    "button",
    "badge"
  ]
}