---
title: Card
description: Content container card with optional link
visibility: guest
draft: false
related:
  - /components/hover-card
  - /components/scroll-area
  - /components/badge
---

# Card

A flexible container for grouping related content. Cards support three visual variants and compose with `header`, `body`, and `footer` sub-components.

> **Wrap content in `<x-wirekit::card.body>` — not directly inside `<x-wirekit::card>`.** The card root is intentionally padding-free so it can host edge-to-edge hero images. Inner padding lives on the three sub-components (`card.header`, `card.body`, `card.footer`). Dropping raw text directly inside `<x-wirekit::card>...</x-wirekit::card>` renders flush against the border — almost certainly not what you want. The sub-components are listed by `php artisan wirekit:show card` and exposed via the `sub_components` field in `wirekit:export-json`. For edge-to-edge content (a flush hero image or full-bleed table) set `<x-wirekit::card.body :padded="false">` to drop the body padding — a raw `<table>` dropped inside an unpadded body still gets readable cell padding automatically.

## Live Sandbox

This is a hydrated playground for the component. Toggle "Live preview" on the block below to swap the static HTML render for a real Livewire instance — every prop in the component's sandbox schema becomes an editable form field inside the iframe, so you can try different prop combinations live without writing any local code.

:::preview{title="Sandbox" sandbox="card" props='{"variant":"outlined","body":"Card body"}'}
<x-wirekit::card>
    <x-wirekit::card.body>Card body</x-wirekit::card.body>
</x-wirekit::card>
:::

## Basic Usage

:::preview{title="Simple card"}
<x-wirekit::card>
    <x-wirekit::card.body>
        A simple card with body content only.
    </x-wirekit::card.body>
</x-wirekit::card>
:::

## Full Composition

:::preview{title="Card with header, body, and footer"}
<x-wirekit::card class="max-w-md">
    <x-wirekit::card.header>Project Settings</x-wirekit::card.header>
    <x-wirekit::card.body>
        Manage your project configuration, team members, and integrations.
    </x-wirekit::card.body>
    <x-wirekit::card.footer>
        <x-wirekit::button size="sm">Save</x-wirekit::button>
    </x-wirekit::card.footer>
</x-wirekit::card>
:::

## Edge-to-edge content

The card root has no padding, so `<x-wirekit::card.body :padded="false">` lets a hero image or full-bleed table run flush to the card's rounded edges. Stack a padded body below it for the copy.

:::preview{title="Edge-to-edge body"}
<x-wirekit::row gap="md" wrap>
    <x-wirekit::card variant="elevated" style="width: 15rem; max-width: 100%">
        <x-wirekit::card.body>
            <x-wirekit::text variant="muted" size="sm">Default body — content sits inside the padding.</x-wirekit::text>
        </x-wirekit::card.body>
    </x-wirekit::card>
    <x-wirekit::card variant="elevated" style="width: 15rem; max-width: 100%">
        <x-wirekit::card.body :padded="false">
            <img src="/placeholder/240x88" alt="" style="width: 100%; display: block" />
        </x-wirekit::card.body>
        <x-wirekit::card.body>
            <x-wirekit::text variant="muted" size="sm">A padded body holds the copy below the flush image.</x-wirekit::text>
        </x-wirekit::card.body>
    </x-wirekit::card>
</x-wirekit::row>
:::

## Letting a child reach past the card

A card clips its content by default, which is what makes the rounded corners cut the
content inside them. The same clip catches anything else that reaches past the box — a
badge pinned to a corner, an avatar that scales on hover, a focus ring that extends past
the edge.

:::preview
<x-wirekit::card overflow="visible" style="max-width: 22rem; position: relative; margin-top: 1.25rem;">
    <x-wirekit::card.body>
        <div style="position: absolute; top: -0.7rem; right: -0.7rem;">
            <x-wirekit::badge intent="danger">3</x-wirekit::badge>
        </div>
        <x-wirekit::stack gap="sm">
            <x-wirekit::heading level="3" size="sm">Reaching past the edge</x-wirekit::heading>
            <x-wirekit::text size="sm" variant="muted">The badge is pinned outside the card's box. With the default <code>overflow="hidden"</code> its top-right corner would be cut off.</x-wirekit::text>
        </x-wirekit::stack>
    </x-wirekit::card.body>
</x-wirekit::card>
:::

Reach for `overflow="visible"` rather than forcing the base class with an `!important`
utility override. The override is not the same thing: it removes the clipping for **every** child at once, which
is harmless while the content is padded and wrong the moment a full-bleed image or a table
goes into that card — and nothing will say so.

`auto` and `clip` are accepted too, for a card whose content should scroll inside it or be
cut without a scrollbar.

::: info
`card.body` sets no overflow of its own, so it does not clip and needs no matching prop. If
you are carrying an override on both the card and its body, the body half is doing nothing.
:::

## Variants

:::preview{title="Outlined, elevated, flat"}
<x-wirekit::card variant="outlined" class="max-w-xs">
    <x-wirekit::card.body>Outlined (default)</x-wirekit::card.body>
</x-wirekit::card>

<x-wirekit::card variant="elevated" class="max-w-xs">
    <x-wirekit::card.body>Elevated (shadow)</x-wirekit::card.body>
</x-wirekit::card>

<x-wirekit::card variant="flat" class="max-w-xs">
    <x-wirekit::card.body>Flat (muted bg)</x-wirekit::card.body>
</x-wirekit::card>
:::

## Clickable Card

Pass `href` to render the card as a link with hover elevation:

:::preview{title="Clickable card"}
<x-wirekit::card href="/products/widget" class="max-w-md">
    <x-wirekit::card.header>Widget Pro</x-wirekit::card.header>
    <x-wirekit::card.body>
        Click anywhere on this card to navigate.
    </x-wirekit::card.body>
</x-wirekit::card>
:::

When `href` is set, the card renders as an `<a>` tag, gets `cursor-pointer`, and shows a stronger shadow on hover.

## Semantic HTML

Use the `as` prop to change the root tag (e.g. for `<article>`, `<section>`):

```blade
<x-wirekit::card as="article">
    <x-wirekit::card.header>Article Title</x-wirekit::card.header>
    <x-wirekit::card.body>Article body content…</x-wirekit::card.body>
</x-wirekit::card>
```

## Width & Layout

Cards are block-level elements that fill their parent width by default. Control the width with Tailwind classes directly on the component:

:::preview{title="Constrained width card"}
<x-wirekit::card class="max-w-md">
    <x-wirekit::card.body>Compact card capped at <code>max-w-md</code> (28 rem).</x-wirekit::card.body>
</x-wirekit::card>
:::

:::preview{title="Three-column card grid"}
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); gap: 1rem;">
    <x-wirekit::card>
        <x-wirekit::card.header>Alpha</x-wirekit::card.header>
        <x-wirekit::card.body>First card in the grid.</x-wirekit::card.body>
    </x-wirekit::card>
    <x-wirekit::card>
        <x-wirekit::card.header>Beta</x-wirekit::card.header>
        <x-wirekit::card.body>Second card in the grid.</x-wirekit::card.body>
    </x-wirekit::card>
    <x-wirekit::card>
        <x-wirekit::card.header>Gamma</x-wirekit::card.header>
        <x-wirekit::card.body>Third card in the grid.</x-wirekit::card.body>
    </x-wirekit::card>
</div>
:::

:::source{language="blade"}
<x-wirekit::grid cols="1 sm:2 lg:3" gap="md">
    <x-wirekit::card>
        <x-wirekit::card.header>Alpha</x-wirekit::card.header>
        <x-wirekit::card.body>First card in the grid.</x-wirekit::card.body>
    </x-wirekit::card>
    <x-wirekit::card>
        <x-wirekit::card.header>Beta</x-wirekit::card.header>
        <x-wirekit::card.body>Second card in the grid.</x-wirekit::card.body>
    </x-wirekit::card>
    <x-wirekit::card>
        <x-wirekit::card.header>Gamma</x-wirekit::card.header>
        <x-wirekit::card.body>Third card in the grid.</x-wirekit::card.body>
    </x-wirekit::card>
</x-wirekit::grid>
:::

In your own code you can of course reach for Tailwind's `grid grid-cols-3 gap-4` utilities directly — the inline styles above are only used to keep the preview compatible with the docs sandbox, which forbids Tailwind utilities on raw HTML.

## Props

### Card Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | string | `'outlined'` | `outlined`, `elevated`, `flat` |
| `as` | string | `'div'` | HTML tag for the root element (ignored if `href` is set) |
| `href` | string\|null | `null` | Renders as `<a>` with hover shadow |
| `overflow` | string | `'hidden'` | What the card does with a child that reaches past its box: `hidden`, `visible`, `auto`, `clip`. The default clips, which is what makes the rounded corners cut the content inside them. See [Letting a child reach past the card](#letting-a-child-reach-past-the-card) |
| `animateIn` | string\|null | `null` | Entrance-reveal preset (e.g. `'fade'`, `'slide-up'`, `'scale-in'`) — inline alternative to wrapping in `<x-wirekit::reveal>`. See [docs/animations.md](../animations.md) for the full preset list. Respects `prefers-reduced-motion: reduce`. |
| `scope` | string\|null | `null` | Scoped personalization name |

### Sub-Components

| Component | Description |
| --- | --- |
| `card.header` | Top section with bottom border, heading typography |
| `card.body` | Main content area with comfortable padding |
| `card.footer` | Bottom section with top border and muted background |

## Accessibility

- When using `href`, the entire card becomes a single focusable link. For cards with multiple interactive elements (buttons, links), prefer leaving the card as a `<div>` and let users click the inner elements individually.
- Use the `as` prop to choose semantic HTML appropriate for your content (`article`, `section`, `aside`).
- Ensure sufficient color contrast when overriding the card background — all default variants meet WCAG AA.

## Keyboard Interaction

This component is purely presentational and does not respond to keyboard input.

## Pitfalls

- **Wrap content in `<x-wirekit::card.body>`.** The card root is a padding-free frame, so raw content placed directly inside renders flush against the border. Use `card.body` (or `card.header` / `card.footer`); in development a console warning flags a card whose content is not wrapped.
- **Don't apply a `dark:` prefix on inner content.** The card's surface token (`--color-wk-bg-elevated`) auto-switches under the `.dark` class — `dark:` overrides bypass the theme.
- **Don't nest a card inside another card for visual emphasis.** Use the `variant` prop (`elevated`, `flat`, `outlined`) for visual weight — nesting compounds shadows and creates a "double frame" anti-pattern.

## Design Tokens

Cards use these tokens for consistent theming:

| Token | Purpose |
| --- | --- |
| `--color-wk-bg-elevated` | Card background (light: white, dark: neutral-900) |
| `--color-wk-bg-subtle` | Flat variant + footer background |
| `--color-wk-border` | Outlined variant border |
| `--color-wk-border-subtle` | Sub-component dividers |
| `--shadow-wk-md` | Elevated variant shadow |
| `--shadow-wk-lg` | Clickable card hover shadow |
| `--radius-wk-lg` | Card corner radius |

All tokens automatically adapt to light/dark mode.

## Customization

Override defaults in `config/wirekit.php`:

```php
'components' => [
    'card' => ['variant' => 'elevated'],
],
```

## Usage & Conventions

> **Prop conventions** — this component uses one or more of the shared semantic prop names (`intent` / `variant` / `tone` / `surface`). See [Prop naming conventions](/extending/prop-naming-conventions) for the canonical vocabulary, alias matrix, and decision tree.

## Further Reading

- [MDN: `<article>` vs `<section>` vs `<div>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article) — when to use each for cards
- [WAI-ARIA Landmark Regions](https://www.w3.org/WAI/ARIA/apg/patterns/landmarks/)
- [Inclusive Components: Cards](https://inclusive-components.design/cards/)
