# Card

> A React card component for listings, stats, settings and plans: media, header, body and footer parts, and a lift-and-press mode.

- Type: React component (Layout)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add card`
- Docs: https://wingo-ui.com/components/card

## Interaction

A plain card just holds content. An interactive card lifts 2px on hover with a deeper shadow while its photo zooms in a touch, and dips to 98% while pressed; pressing a button inside it (a like, a menu) presses only that button. Link cards are one Tab stop with the focus ring around the whole card: Enter opens them, a click anywhere follows the link, and the buttons on the photo, in the header and in the footer keep working. A card with onClick is a real button to the keyboard (Enter or Space). Selectable cards built on a radio group move with the arrow keys and show a ring in the tone. A disabled card dims and leaves the tab order. Phones get no hover lift, only the press, and horizontal cards stack their photo on top below 640px.

## Installation

```bash
npx wingo-ui@latest add card
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/card.json`

Files:

- `components/ui/card.tsx`

npm dependencies: `npm i @radix-ui/react-slot`

Wingo UI dependencies (installed automatically):

- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { Heart } from "lucide-react"
import { Avatar } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardFooter, CardLink, CardMedia } from "@/components/ui/card"

export function ListingCard({ onSave }: { onSave: () => void }) {
  return (
    <Card
      className="max-w-sm"
      media={
        <CardMedia src="/demo/apartment.jpg">
          <Badge variant="solid" size="sm" className="absolute start-3 top-3">New</Badge>
          <Button size="icon-sm" variant="soft" radius="full" aria-label="Save" onClick={onSave} className="absolute end-3 top-3">
            <Heart />
          </Button>
        </CardMedia>
      }
      title={<CardLink href="/listings/10482">2-bedroom apartment, Park Slope</CardLink>}
      description="Brooklyn · 580 sq ft · floor 3 of 4"
      footer={
        <CardFooter align="between">
          <span className="text-[17px] font-semibold">$445,000</span>
          <span className="flex items-center gap-2 text-[13px] text-muted-foreground">
            <Avatar src="/demo/ana-pop.jpg" name="Anna Price" size="xs" /> today, 2:20 PM
          </span>
        </CardFooter>
      }
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title?` | `ReactNode` | `""` | Simple API: the heading (an h3). Pass a CardLink here to make the whole card a link (interactive turns on by itself). The demo shows its sample listing title while empty. |
| `description?` | `ReactNode` | `""` | Simple API: the muted line under the title. The demo shows its sample while empty. |
| `variant?` | `"elevated" \| "outline" \| "soft" \| "ghost"` | `"elevated"` | elevated is a raised white panel (the surface token with its shadow), outline a hairline frame, soft the flat gray tile (tinted by a tone), ghost only padding, for lists of cards on a panel. Deviation: elevated is a card-only value. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Tints soft and outline cards (the title takes the tone color) and colors the selected ring. Elevated and ghost cards only show it when selected. |
| `color?` | `string` | `""` | Any CSS color. Replaces the tone: a 14% tint on soft cards, the border on outline cards, the selected ring. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Padding 16 / 20 / 24px (md and lg use one step less on phones), 12 / 16 / 20px between parts, and the title size. Media bleeds and divided footers follow. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner radius: 0, 12, 16 or 24px; full falls back to 24px. Inset media takes the radius minus the padding. |
| `interactive?` | `boolean` | `false` | Hover lift (2px and a floating shadow, a tint on ghost cards), a 0.98 press, pointer cursor and the focus ring; media zooms on hover. On by default with onClick, with a CardLink as the title prop, and with asChild onto a link, button or label. With the parts, set it next to a CardLink so the card really goes somewhere. |
| `selected?` | `boolean` | `false` | A ring in the tone around selectable cards (plans, addresses, payment methods). With onClick the card also reports aria-pressed. |
| `disabled?` | `boolean` | `false` | Dims the card and blocks the pointer, the lift and the press. Link and button cards, and a CardLink inside, get aria-disabled, leave the tab order and ignore clicks; disable other controls inside it yourself. |
| `orientation?` | `"vertical" \| "horizontal"` | `"vertical"` | horizontal puts the media at the start from 640px up, bleeding to the top, bottom and start edges; phones keep it on top. With parts, wrap the rest in CardContent. |
| `divided?` | `boolean` | `false` | A hairline above the footer, edge to edge (stopping at start media). The default for every CardFooter inside, which can still override it. |
| `asChild?` | `boolean` | `false` | Renders your single child (an <a>, a router Link, a <label> around a radio) as the card, with its look and press. A link card cannot hold buttons; use a CardLink title for that. The demo swaps in a link and drops the like button. |
| `children?` | `ReactNode` |  | With the simple props, the body (inside CardBody). Without them, the parts: CardMedia, CardHeader, CardBody, CardFooter. |
| `action?` | `ReactNode` |  | Simple API: a control at the end of the header (a menu, a like, a badge). It stays clickable above a CardLink. |
| `media?` | `ReactNode` |  | Simple API: an image URL, a <CardMedia> (for aspect, overlays, inset), or any node (a chart, a map), which fills the media slot at its own height. |
| `footer?` | `ReactNode` |  | Simple API: footer content (end-aligned), or a <CardFooter> for its own align and divided. |
| `onClick?` | `(event: MouseEvent<HTMLDivElement>) => void` |  | Makes the card a button: role=button, a Tab stop, Enter and Space, interactive on. Not called while disabled. Prefer asChild with a real <a> for navigation. |
| `classNames?` | `Partial<Record<"media" \| "content" \| "header" \| "title" \| "description" \| "action" \| "body" \| "footer", string>>` |  | Extra classes for the parts the simple API renders. |
| `className?` | `string` |  | Extra classes for the card itself, merged last. Override the spacing vars here, e.g. [--card-padding:12px]. |

## Parts

### CardMedia (component)

Photo or custom media that bleeds to the card edges (it reads --card-padding). Its children sit on top of the photo (a badge, a like button; direct child controls stay clickable above a CardLink), or are the media when there is no src.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `src?` | `string` |  | Image URL, drawn with object-cover. |
| `alt?` | `string` | `""` | Alt text; empty marks the photo decorative. Describe it only when it says something the text does not. |
| `aspect?` | `"video" \| "square" \| "4/3" \| "16/10" \| "auto"` | `"16/10"` | The box ratio; auto takes the content's own height. |
| `position?` | `"top" \| "start"` |  | Where the media sits; inferred from the card's orientation. |
| `inset?` | `boolean` | `false` | Keeps the card padding around the media and rounds its corners instead of bleeding to the edges. |
| `zoom?` | `boolean` | `true` | Scales the photo up a touch on hover in interactive cards. |
| `loading?` | `"lazy" \| "eager"` | `"lazy"` | The image loading strategy. |
| `children?` | `ReactNode` |  | Overlays on the photo, or the media itself (a chart, a map) when there is no src. |
| `classNames?` | `Partial<Record<"image", string>>` |  | Extra classes for the img. |

### CardHeader (component)

Grid with CardTitle and CardDescription on the start and CardAction at the end, centered on a lone title and top-aligned on two lines. Takes div props.

### CardTitle (component)

An h3 (15px semibold at md, 14 / 17px at sm / lg) that takes the tone color on tinted cards. Takes h3 props.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild?` | `boolean` | `false` | Renders your child (another heading level) with the title styles. |

### CardDescription (component)

The muted line under the title; deepens on tinted cards so it keeps 4.5:1. Takes p props.

### CardAction (component)

The header's end column for a control; relative z-[2], so it stays clickable above a CardLink. Takes div props.

### CardBody (component)

Body text at the card's size (13 / 14 / 15px). Takes div props.

### CardFooter (component)

Actions or meta in a row, pinned to the bottom when a grid row stretches the card; its direct buttons and links stay clickable above a CardLink. Takes div props.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align?` | `"start" \| "end" \| "between"` | `"end"` | How the footer content is distributed. |
| `divided?` | `boolean` |  | An edge-to-edge hairline above the footer; defaults to the card's divided. |

### CardContent (component)

The column next to start media in a horizontal card (the simple API always renders it). Takes div props.

### CardLink (component)

A stretched link for the title: its ::after covers the card, so a click anywhere opens it (new tab, middle-click and long-press menus included) while CardAction and the direct controls of CardMedia and CardFooter stay separate (give other controls relative z-[2]). As the title prop it is described by the description prop, so a screen reader reads both. The card shows the focus ring; in a disabled card the link leaves the tab order. Takes a props.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild?` | `boolean` | `false` | Renders a router Link (or any anchor component) as the stretched link. |

## Examples

- **Variants**: elevated on a page, outline for quiet frames, soft as the flat gray tile, ghost for padding only.
- **Stat cards**: onClick makes a card a keyboard button (Enter, Space); selected rings the metric in focus.
- **Settings with a divided footer**: The footer hairline runs edge to edge; Save stays disabled until something changes.
- **Horizontal link card**: Media at the start from 640px up, on top on phones. The title is a CardLink; the phone button stays its own button.
- **Link cards**: A CardLink title stretches over the card: one Tab stop, the ring around the whole card, native new-tab.
- **Selectable plans**: A native radio group: arrow keys move the choice, the selected card takes the brand ring.
- **Tinted**: soft and outline cards take a tone or any color; the title follows and the text keeps its contrast.
- **Sizes**: Padding 16, 20 and 24px (one step less on phones) with the title scaling along.

## See also

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [Skeleton](https://wingo-ui.com/components/skeleton) ([Markdown](https://wingo-ui.com/components/skeleton.md))
- [Separator](https://wingo-ui.com/components/separator) ([Markdown](https://wingo-ui.com/components/separator.md))

---

Canonical: https://wingo-ui.com/components/card
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
