# Card

Displays a card with header, content, and footer.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { HStack, VStack } from "@/components/ui/stack"
import { color, space } from "@/theme/tokens.stylex"
import { ChevronRight } from "lucide-react"

const styles = stylex.create({
  card: { width: "100%", maxWidth: "24rem" },
  footer: { flexDirection: "column", rowGap: space.s2, alignItems: "stretch" },
  chevron: {
    width: space.s4,
    height: space.s4,
    flexShrink: 0,
    marginBlockStart: space.s0_5,
    color: color.mutedForeground,
  },
})

export default function CardDemo() {
  return (
    <Card xstyle={styles.card}>
      <CardHeader>
        <CardTitle>Scheduled reports</CardTitle>
        <CardDescription>Weekly snapshots. No more manual exports.</CardDescription>
        <CardAction>
          <Badge variant="secondary">Beta</Badge>
        </CardAction>
      </CardHeader>
      <CardContent>
        <VStack gap={2}>
          <HStack gap={2}>
            <ChevronRight {...stylex.props(styles.chevron)} />
            <span>Choose a schedule, daily or weekly.</span>
          </HStack>
          <HStack gap={2}>
            <ChevronRight {...stylex.props(styles.chevron)} />
            <span>Send to channels or specific teammates.</span>
          </HStack>
          <HStack gap={2}>
            <ChevronRight {...stylex.props(styles.chevron)} />
            <span>Include charts, tables, and key metrics.</span>
          </HStack>
        </VStack>
      </CardContent>
      <CardFooter xstyle={styles.footer}>
        <Button>Set up scheduled reports</Button>
        <Button variant="secondary">See what&apos;s new</Button>
      </CardFooter>
    </Card>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and copy `states.ts` from the same page
   for the footer's tray. The card also needs `interop.css`, which pulls a leading
   image to the top edge.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/card.tsx"
   "use client"

   import { useRender } from "@base-ui/react/use-render"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { states } from "@/theme/states"
   import { color, leading, radius, shadow, space, text } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     root: {
       display: "flex",
       flexDirection: "column",
       "--carbon-card-spacing": space.s4,
       "--carbon-card-title-size": text.base,
       rowGap: "var(--carbon-card-spacing)",
       paddingBlock: "var(--carbon-card-spacing)",
       overflow: "hidden",
       borderRadius: radius.xl,
       backgroundColor: color.card,
       color: color.cardForeground,
       boxShadow: shadow.raised,
       fontSize: text.sm,
       lineHeight: leading.sm,
     },
     header: {
       display: "grid",
       gridTemplateColumns: "1fr auto",
       gridAutoRows: "min-content",
       alignItems: "start",
       rowGap: space.s1,
       paddingInline: "var(--carbon-card-spacing)",
     },
     title: {
       gridColumnStart: "1",
       fontSize: "var(--carbon-card-title-size)",
       lineHeight: leading.base,
       fontWeight: 500,
     },
     description: { gridColumnStart: "1", color: color.mutedForeground },
     action: {
       gridColumnStart: "2",
       gridRowStart: "1",
       gridRowEnd: "span 2",
       alignSelf: "start",
       justifySelf: "end",
     },
     content: { paddingInline: "var(--carbon-card-spacing)" },
     footer: {
       display: "flex",
       alignItems: "center",
       "--carbon-tray-spacing": "calc(var(--carbon-card-spacing) * 0.75)",
       "--carbon-tray-body-bg": color.card,
       marginBlockStart: `calc(-1 * ${radius.xl})`,
       marginBlockEnd: "calc(-1 * var(--carbon-card-spacing))",
     },
   })

   const sizes = stylex.create({
     default: {},
     sm: {
       "--carbon-card-spacing": space.s3,
       "--carbon-card-title-size": text.sm,
     },
   })

   const cardStyles = {
     base: styles.root,
     sizes,
     header: styles.header,
     title: styles.title,
     description: styles.description,
     action: styles.action,
     content: styles.content,
     footer: [states.trayFooter, styles.footer],
   }

   type CardSize = keyof typeof sizes

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type CardProps = useRender.ComponentProps<"div"> & Overrides & { size?: CardSize }

   function Card({ render, ref, style, size = "default", xstyle, className, ...props }: CardProps) {
     const sx = stylex.props(styles.root, sizes[size], xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "card",
         "data-size": size,
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function CardHeader({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & Overrides) {
     const sx = stylex.props(styles.header, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "card-header",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function CardTitle({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & Overrides) {
     const sx = stylex.props(styles.title, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "card-title",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function CardDescription({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & Overrides) {
     const sx = stylex.props(styles.description, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "card-description",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function CardAction({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & Overrides) {
     const sx = stylex.props(styles.action, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "card-action",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function CardContent({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & Overrides) {
     const sx = stylex.props(styles.content, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "card-content",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function CardFooter({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & Overrides) {
     const sx = stylex.props(states.trayFooter, styles.footer, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "card-footer",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   export {
     Card,
     CardAction,
     CardContent,
     CardDescription,
     CardFooter,
     CardHeader,
     CardTitle,
     cardStyles,
     type CardSize,
   }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
```

```tsx
<Card>
  <CardHeader>
    <CardTitle>Card Title</CardTitle>
    <CardDescription>Card Description</CardDescription>
    <CardAction>Card Action</CardAction>
  </CardHeader>
  <CardContent>
    <p>Card Content</p>
  </CardContent>
  <CardFooter>
    <p>Card Footer</p>
  </CardFooter>
</Card>
```

## Composition

```text
Card
├── CardHeader
│   ├── CardTitle
│   ├── CardDescription
│   └── CardAction
├── CardContent
└── CardFooter
```

`CardHeader` is a two-column grid. `CardAction` sits in the second column and
spans both rows, so a title and description stay to its left.

## Size

Use `size="sm"` for a tighter inset and a title one step down.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { HStack } from "@/components/ui/stack"

const styles = stylex.create({
  card: { width: "100%", maxWidth: "18rem" },
})

export default function CardSize() {
  return (
    <HStack align="start" gap={4} wrap="wrap" justify="center">
      <Card size="default" xstyle={styles.card}>
        <CardHeader>
          <CardTitle>Default</CardTitle>
          <CardDescription>Sections sit 16px from the edge.</CardDescription>
        </CardHeader>
        <CardContent>Weekly snapshots. No more manual exports.</CardContent>
        <CardFooter>
          <Button>Enable</Button>
        </CardFooter>
      </Card>
      <Card size="sm" xstyle={styles.card}>
        <CardHeader>
          <CardTitle>Small</CardTitle>
          <CardDescription>12px, and a title one step down.</CardDescription>
        </CardHeader>
        <CardContent>Weekly snapshots. No more manual exports.</CardContent>
        <CardFooter>
          <Button size="sm">Enable</Button>
        </CardFooter>
      </Card>
    </HStack>
  )
}
```

## Spacing

Set `--carbon-card-spacing` to change the gap between sections and the inset of
every part at once.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { HStack } from "@/components/ui/stack"
import { space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  card: { width: "100%", maxWidth: "18rem" },
  roomy: { width: "100%", maxWidth: "18rem", "--carbon-card-spacing": space.s8 },
})

export default function CardSpacing() {
  return (
    <HStack align="start" gap={4} wrap="wrap" justify="center">
      <Card xstyle={styles.card}>
        <CardHeader>
          <CardTitle>Terms of Service</CardTitle>
          <CardDescription>Review the terms before accepting.</CardDescription>
        </CardHeader>
        <CardContent>The default inset is 16px.</CardContent>
        <CardFooter>
          <Button>Accept</Button>
        </CardFooter>
      </Card>
      <Card xstyle={styles.roomy}>
        <CardHeader>
          <CardTitle>Terms of Service</CardTitle>
          <CardDescription>Review the terms before accepting.</CardDescription>
        </CardHeader>
        <CardContent>This one sets 32px.</CardContent>
        <CardFooter>
          <Button>Accept</Button>
        </CardFooter>
      </Card>
    </HStack>
  )
}
```

Bleed content to the card's edges with a negative
`margin-inline: calc(-1 * var(--carbon-card-spacing))`, then put the same value
back as padding to keep the text on the card's inset. When that content sits
above a footer, cancel the section gap less the footer's raised strip with
`margin-block-end: calc(radius.xl - var(--carbon-card-spacing))` on
`CardContent`.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { VStack } from "@/components/ui/stack"
import { leading, radius, space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  card: { width: "100%", maxWidth: "24rem" },
  content: { marginBlockEnd: `calc(${radius.xl} - var(--carbon-card-spacing))` },
  panel: {
    marginInline: "calc(-1 * var(--carbon-card-spacing))",
    paddingInline: "var(--carbon-card-spacing)",
    paddingBlock: space.s4,
    maxHeight: "12rem",
    overflowY: "auto",
    borderBlockStartWidth: 1,
    borderBlockStartStyle: "solid",
    lineHeight: leading.base,
  },
  footer: { justifyContent: "flex-end", columnGap: space.s2 },
})

export default function CardEdgeToEdge() {
  return (
    <Card xstyle={styles.card}>
      <CardHeader>
        <CardTitle>Terms of Service</CardTitle>
        <CardDescription>Review the terms before accepting the agreement.</CardDescription>
      </CardHeader>
      <CardContent xstyle={styles.content}>
        <VStack gap={4} xstyle={styles.panel}>
          <p>
            These terms govern your use of the workspace, including access to shared documents,
            project files, and collaboration tools.
          </p>
          <p>
            You are responsible for the content you upload and for ensuring that your team has the
            appropriate permissions to view or edit it.
          </p>
          <p>
            We may update features or limits as the service evolves. When those changes materially
            affect your workflow, we will notify your workspace administrators.
          </p>
        </VStack>
      </CardContent>
      <CardFooter xstyle={styles.footer}>
        <Button variant="secondary">Decline</Button>
        <Button>Accept</Button>
      </CardFooter>
    </Card>
  )
}
```

## Image

Put an `img` first in the card. It runs to the top edge and its corners clip to
the card's radius.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { space } from "@/theme/tokens.stylex"

const styles = stylex.create({
  card: { width: "100%", maxWidth: "24rem" },
  cover: { width: "100%", aspectRatio: "16 / 9", objectFit: "cover" },
  footer: { columnGap: space.s2 },
})

export default function CardImage() {
  return (
    <Card xstyle={styles.card}>
      <img
        src="https://avatar.vercel.sh/carbon"
        alt="Event cover"
        {...stylex.props(styles.cover)}
      />
      <CardHeader>
        <CardAction>
          <Badge variant="secondary">Featured</Badge>
        </CardAction>
        <CardTitle>Design systems meetup</CardTitle>
        <CardDescription>
          A practical talk on component APIs, accessibility, and shipping faster.
        </CardDescription>
      </CardHeader>
      <CardFooter xstyle={styles.footer}>
        <Button>View event</Button>
        <Button variant="secondary">Add to calendar</Button>
      </CardFooter>
    </Card>
  )
}
```

> **Note**
>
> `interop.css` pulls the image up with a negative margin keyed on
> `[data-slot="card"] > img:first-child`. Without that stylesheet the image sits
> inside the card's top padding.

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part renders a `div` and takes `xstyle` and `className` alongside every
`div` attribute.

### Card

| Prop        | Type                | Default     |
| ----------- | ------------------- | ----------- |
| `size`      | `"default" \| "sm"` | `"default"` |
| `xstyle`    | `StyleXStyles`      | —           |
| `className` | `string`            | —           |

`size` sets `--carbon-card-spacing` and `--carbon-card-title-size` on the root.
Setting either directly overrides it.

### CardHeader

A grid of `1fr auto`, with a `0.25rem` row gap. The second column collapses to
nothing without a `CardAction`.

### CardTitle

Reads `--carbon-card-title-size` at weight 500.

### CardDescription

Muted text at the card's own size.

### CardAction

Pinned to the header's second column and first row, spanning both rows.

### CardContent

The card's inset, and nothing else. Vertical rhythm is the root's row gap.

### CardFooter

A tray under the card's body. The body's bottom edge rounds over the tray with a
hairline shadow. The footer draws that shadow itself, so the body needs no
wrapper. It cancels the root's bottom padding, so put it last.

### cardStyles

The style maps behind the component. Spread them onto any element.

| Key           | What it is                                      |
| ------------- | ----------------------------------------------- |
| `base`        | The surface: fill, ring, radius, shadow, rhythm |
| `sizes`       | `default` and `sm`, each a pair of variables    |
| `header`      | The two-column grid                             |
| `title`       | The title's type                                |
| `description` | The muted description                           |
| `action`      | The header's second column                      |
| `content`     | The body's inset                                |
| `footer`      | The tray, its raised strip and negative margins |

### Data attributes

| Attribute                      | On              | Effect                                   |
| ------------------------------ | --------------- | ---------------------------------------- |
| `data-slot="card"`             | the root        | Where `interop.css` keys a leading image |
| `data-size`                    | the root        | Reflects the `size` prop                 |
| `data-slot="card-header"`      | the header      | Targets the header from a parent         |
| `data-slot="card-title"`       | the title       | Targets the title from a parent          |
| `data-slot="card-description"` | the description | Targets the description from a parent    |
| `data-slot="card-action"`      | the action      | Targets the action from a parent         |
| `data-slot="card-content"`     | the body        | Targets the body from a parent           |
| `data-slot="card-footer"`      | the footer      | Targets the footer from a parent         |

### CSS variables

| Variable                   | On       | Effect                                                     |
| -------------------------- | -------- | ---------------------------------------------------------- |
| `--carbon-card-spacing`    | the root | The section gap and every part's inset. 16px, 12px at `sm` |
| `--carbon-card-title-size` | the root | The title's size. 16px, 14px at `sm`                       |
