# Card

> Part of mCSS (mcss.dev). Rendered page: https://mcss.dev/components/card

The `.card` component, in its simplest form, is a container with 3 style options:

- **Default:** transparent background, border outline.
- **Filled:** solid background color.
- **Raised:** drop shadow and hover effect.

Every part is optional: header with title and subtitle, image (inset, or edge-to-edge with the flush modifier), badges, tags, byline with avatar and date, actions footer, clickable body, disabled state, and loading state with skeleton UI. The Astro component makes it easy to use all those features, and `CardList` lays cards out in equal-height responsive columns. This site's own [blog index](/blog) is built from exactly these two components.

<div class="grid" col="1" col-md="2">
  <div class="grid_item">
    <Card variant="filled" title="A simple filled card">
      <p>Natural Law, is a set of universal, non-man-made, binding and unchangeable conditions which govern the consequence of our actions.</p>
    </Card>
  </div>
  <div class="grid_item">
    <Card variant="raised" title="A simple raised card">
      <p>Natural Law, is a set of universal, non-man-made, binding and unchangeable conditions which govern the consequence of our actions.</p>
    </Card>
  </div>
</div>

<div class="docs_oversizedTable">

| File                                | Description                        | Source      |
| ----------------------------------- | ---------------------------------- | ----------- |
| `component.card.css`                | All card styles (`.card`)          | [Github][1] |
| `component.cardList.css`            | Equal-height list layout           | [Github][2] |
| `Card.astro`, `CardList.astro`      | The Astro components               | [Github][3] |
| `Tags.astro`, `Avatar.astro`        | Composed components (dependencies) | [Github][3] |
| `--card-*` block in `settings.ui.css`   | Interface tokens (see below)           | [Github][4] |

</div>

[1]: https://github.com/minimaldesign/mCSS/blob/main/src/styles/framework/component.card.css
[2]: https://github.com/minimaldesign/mCSS/blob/main/src/styles/framework/component.cardList.css
[3]: https://github.com/minimaldesign/mCSS/tree/main/src/components
[4]: https://github.com/minimaldesign/mCSS/blob/main/src/styles/framework/settings.ui.css

## Playground

<Playground
  client:visible
  template={`<article class="{classes}">{media}{tags}
  <header class="card_header">
    <div class="card_header_text">
      <h2 class="card_title">{title}</h2>
      <p>{subtitle}</p>
    </div>{badges}
  </header>
  <section class="card_content">
    <p>{body}</p>
  </section>{byline}{actions}
</article>`}
  baseClasses="card"
  snippets={{
    mediaInset: {
      preview: `<figure class="card_media"><img class="card_image" src="${placeholder.src}" alt="" loading="lazy" /></figure>`,
      code: `<figure class="card_media"><img class="card_image" src="image.jpg" alt="" loading="lazy" /></figure>`,
    },
    mediaFlush: {
      preview: `<figure class="card_media card_media-flush"><img class="card_image" src="${placeholder.src}" alt="" loading="lazy" /></figure>`,
      code: `<figure class="card_media card_media-flush"><img class="card_image" src="cover.jpg" alt="" loading="lazy" /></figure>`,
    },
    badges: {
      preview: '<div class="card_badges"><span class="badge card_badge">new</span></div>',
      code: '<div class="card_badges"><span class="badge card_badge">new</span></div>',
    },
    tags: {
      preview: '<nav aria-label="Tags"><ul class="tags tags-inline card_tags"><li><a href="#">css</a></li><li><a href="#">learn</a></li></ul></nav>',
      code: '<nav aria-label="Tags"><ul class="tags tags-inline card_tags"><li><a href="#">css</a></li><li><a href="#">learn</a></li></ul></nav>',
    },
    byline: {
      preview: '<footer class="card_byline"><div class="avatar avatar-sm"><div><span>SM</span></div></div><div><span class="card_author">Sonny Mueller</span><time datetime="2026-05-02">May 2, 2026</time></div></footer>',
      code: '<footer class="card_byline"><div class="avatar avatar-sm"><div><span>SM</span></div></div><div><span class="card_author">Sonny Mueller</span><time datetime="2026-05-02">May 2, 2026</time></div></footer>',
    },
    actions: {
      preview: '<footer class="card_actions"><a class="bt bt-text" href="#">Details</a><button class="bt bt-primary">Add to cart</button></footer>',
      code: '<footer class="card_actions"><a class="bt bt-text" href="#">Details</a><button class="bt bt-primary">Add to cart</button></footer>',
    },
  }}
  controls={[
    { heading: "Variation", items: [
      { type: "select", name: "variant", label: "Variant", default: "", options: [
        { label: "Default", value: "" },
        { label: "Filled", value: "card-filled" },
        { label: "Raised", value: "card-raised" },
      ]},
      { type: "select", name: "media", label: "Image", default: "", snippet: "media", options: [
        { label: "None", value: "" },
        { label: "Inset", value: "inset", class: "", snippet: "mediaInset" },
        { label: "Flush", value: "flush", class: "", snippet: "mediaFlush" },
      ]},
    ]},
    { heading: "Parts", items: [
      { type: "checkbox", name: "badges", label: "Badges", snippet: "badges", default: false },
      { type: "checkbox", name: "tags", label: "Tags", snippet: "tags", default: false },
      { type: "checkbox", name: "byline", label: "Byline", snippet: "byline", default: false },
      { type: "checkbox", name: "actions", label: "Actions", snippet: "actions", default: false },
    ]},
    { heading: "State", items: [
      { type: "checkbox", name: "disabled", label: "Disabled", value: "is-disabled", default: false },
    ]},
    { heading: "Content", items: [
      { type: "text", name: "title", label: "Title", default: "Card title" },
      { type: "text", name: "subtitle", label: "Subtitle", default: "Category" },
      { type: "text", name: "body", label: "Body", default: "This is where the content goes!" },
    ]},
  ]}
/>

## HTML

### Available modifiers

<div class="docs_oversizedTable">

| Available modifiers                | Description                                        |
| ---------------------------------- | -------------------------------------------------- |
| `.card`                            | Default style. Transparent background with border. |
| `.card` + `.card-filled`           | Filled with a background color.                    |
| `.card` + `.card-raised`           | Drop shadow, animated on hover.                    |
| `.card` + `.is-disabled`           | Lowered opacity. Interactivity disabled.           |
| `.card_media` + `.card_media-flush`| Edge-to-edge image on top of the card.             |

</div>

### Clickable cards

A card carries **one** link: put `.card_link` on a real anchor (usually the title link) and it stretches over the whole card with a `::after`, so screen readers hear one link named by the title. Actions stay clickable because `.card_actions` is positioned and comes later in the DOM; tags stay clickable through their own z-index bump. The blog-index pattern combines this with a flush image, tags, and a byline, wrapped in a `.cardList` over the [grid system](/docs/global#grid):

```html
<ul class="cardList grid" col="1" col-md="2" col-lg="2">
  <li>
    <article class="card">
      <figure class="card_media card_media-flush">
        <img class="card_image" src="cover.jpg" alt="" loading="lazy" />
      </figure>
      <nav aria-label="Tags"><ul class="tags tags-inline card_tags">
        <li><a href="/blog/tags/css">css</a></li>
      </ul></nav>
      <header class="card_header">
        <div class="card_header_text">
          <h3 class="card_title"><a class="card_link" href="/blog/my-post">Designing with cascade layers</a></h3>
        </div>
      </header>
      <div class="card_content"><p>Why layer order beats specificity wars.</p></div>
      <footer class="card_byline">
        <!-- avatar markup, see the Avatar docs -->
        <div>
          <span class="card_author">Sonny Mueller</span>
          <time datetime="2026-05-02">May 2, 2026</time>
        </div>
      </footer>
    </article>
  </li>
</ul>
```

### Custom properties

The following custom properties are available in [`settings.ui.css`](/docs/tokens#interface-tokens):

<div class="docs_oversizedTable">

| Property                      | Color                                        |
| ----------------------------- | -------------------------------------------- |
| `--card-spacing`              | General padding inside the card.             |
| `--card-color`                | Text color.                                  |
| `--card-secondary-text-color` | Subtitle, date, and secondary text color.    |
| `--card-bg-color`             | Background color of the "filled" variant.    |
| `--card-border-radius`        | Border radius.                               |
| `--card-border-color`         | Border color.                                |
| `--card-border-width`         | Border width.                                |
| `--card-shadow`               | Default drop shadow or the "raised" variant. |
| `--card-shadow-hover`         | Hover state drop shadow.                     |
| `--card-aspect-ratio`         | Image aspect ratio (defaults to `--ar-golden`). |
| `--card-loading-bg`           | Loading skeleton background color.           |

</div>

Badge colors come from the [badge component](/components/badge)'s `--badge-*` tokens.

## Astro component

**Card**

Every part is optional; render only what you pass. Badges and tags can coexist, but they compete for attention: badges are non-interactive status labels pinned top-right, tags are taxonomy links in the content flow. Prefer one or the other on a given card.

<div class="docs_oversizedTable prose">

| Prop           | Type                      | Default        | Description                                                         |
| -------------- | ------------------------- | -------------- | ------------------------------------------------------------------- |
| `variant`      | `string`                  | undefined      | Card’s visual style: `filled`, `raised`.                            |
| `isDisabled`   | `boolean`                 | `false`        | Disabled style. Disabled interactions.                              |
| `isLoading`    | `boolean`                 | `false`        | Loading state with skeleton UI.                                     |
| `href`         | `string`                  | undefined      | Makes the whole card clickable (through the title link, if any).    |
| `title`        | `string`                  | undefined      | Header title text.                                                  |
| `subtitle`     | `string`                  | undefined      | Header subtitle text.                                               |
| `headingLevel` | `number`                  | `2`            | Heading level of the title: `2`, `3`, or `4`.                       |
| `imageSrc`     | `ImageMetadata \| string` | undefined      | Add an image to the card (imported asset or URL/path).              |
| `imageAlt`     | `string`                  | `''`           | Alt text for the image.                                             |
| `aspectRatio`  | `string`                  | `golden`       | Image aspect ratio (available [aspect ratios][ar]).                 |
| `flushImage`   | `boolean`                 | `false`        | Edge-to-edge image on top of the card instead of inset.             |
| `badges`       | `array`                   | `[]`           | Array of objects (see example below).                               |
| `tags`         | `string[]`                | `[]`           | Tag names, rendered with the Tags component.                        |
| `tagsUrl`      | `string`                  | `'/blog/tags'` | Base URL for tag links.                                             |
| `author`       | `string`                  | undefined      | Byline name (also feeds the Avatar initials).                       |
| `avatarSrc`    | `ImageMetadata \| string` | undefined      | Byline avatar image.                                                |
| `date`         | `Date`                    | undefined      | Publication date, rendered as a `<time>` in the byline.             |
| `locale`       | `string`                  | `'en-US'`      | Locale for the formatted date.                                      |
| `data-testid`  | `string`                  | `'card'`       | Test ID for testing frameworks.                                     |
| `class`        | `string`                  | undefined      | Additional CSS classes, useful for [helper classes](/docs/helpers). |

[ar]: /docs/tokens#aspect-ratio

</div>

**CardList**

A `ul` over the [grid system](/docs/global#grid) whose items stretch, so cards in a row share the same height. Children go in the default slot; wrap each card in an `<li>`.

<div class="docs_oversizedTable prose">

| Prop   | Type     | Default | Description                            |
| ------ | -------- | ------- | -------------------------------------- |
| `cols` | `number` | `2`     | Columns from `--lg`: `1`, `2`, or `3`. |

</div>

### Slots

The `Card` component supports the following slots for content projection:

- **Default slot**: Main content area (e.g., text or paragraphs).
- **Actions slot**: Action buttons or links in the footer.

It is recommended to use the [button component](/components/button) in the `actions` slot, e.g.:

```astro
<Fragment slot="actions">
  <a class="bt bt-text" href="/home">Cancel</a>
  <button class="bt bt-primary">Add to Cart</button>
</Fragment>
```

### Badge Styling

The `badges` prop takes an array of objects. Each object consists of a `label` property which defines the badge's text, and an optional `color` property that defines the badge's background color. If no `color` is provided, it defaults to `--badge-background-color` from the [badge component](/components/badge).

<div class="grid" col="1" col-md="3">
  <div class="grid_item" span="2">
    <Card
      title="A card with too many badges"
      badges={[
        { label: "Way", color: "#a6b65a" },
        { label: "Too", color: "#86615c" },
        { label: "Many", color: "#81a598" },
        { label: "Badges", color: "#e3863d" },
      ]}
    >
      <p>This is where the content goes. Unless you only want a bunch of badges!</p>
    </Card>
  </div>
</div>

```astro
<Card
  title="A card with too many badges"
  badges={[
    { label: "Way", color: "#a6b65a" },
    { label: "Too", color: "#86615c" },
    { label: "Many", color: "#81a598" },
    { label: "Badges", color: "#e3863d" },
  ]}
>
  <p>This is where the content goes. Unless you only want a bunch of badges!</p>
</Card>
```

## Examples

<ul class="docs_examples">

<li>

### Blog-index pattern

The article-card recipe: flush image, tags, stretched title link, description, and byline, in a `CardList`.

<CardList cols={2}>
  <li>
    <Card href="#" headingLevel={3} flushImage imageSrc={placeholder.src} title="Designing with cascade layers" tags={["css", "learn"]} author="Sonny Mueller" date={new Date("2026-05-02")}>
      <p>Why layer order beats specificity wars, and how mCSS hands the final word to your unlayered CSS.</p>
    </Card>
  </li>
  <li>
    <Card href="#" headingLevel={3} flushImage imageSrc={placeholder.src} title="Tokens all the way down" tags={["css"]} author="Ada Chen" date={new Date("2026-06-11")}>
      <p>One file of custom properties is the whole theming story.</p>
    </Card>
  </li>
</CardList>

```astro
---
import Card from '../components/Card.astro';
import CardList from '../components/CardList.astro';
---
<CardList cols={2}>
  <li>
    <Card
      href="/blog/my-post"
      headingLevel={3}
      flushImage
      imageSrc={cover}
      title="Designing with cascade layers"
      tags={["css", "learn"]}
      author="Sonny Mueller"
      date={new Date("2026-05-02")}
    >
      <p>Why layer order beats specificity wars.</p>
    </Card>
  </li>
</CardList>
```

</li>
<li>

### Card with image and actions

<div class="grid" col="1" col-md="2">
  <div class="grid_item" span="1">
    <Card
      variant="raised"
      href="#"
      title="Product Name"
      subtitle="Category"
      imageSrc={placeholder}
      imageAlt="Product image"
      aspectRatio="widescreen"
    >
      <p>This is a extremely detailed description of the product.</p>
      <Fragment slot="actions">
        <a class="bt bt-md bt-text " href="#">
          View Details
        </a>
        <button class="bt bt-md bt-primary">Add to Cart</button>
      </Fragment>
    </Card>
  </div>
</div>

```astro
---
import placeholder from "../../assets/images/image.png";
import Card from '../../components/Card.astro';
---
<Card
  variant="raised"
  href="/product/123"
  title="Product Name"
  subtitle="Category"
  imageSrc={placeholder}
  imageAlt="Product image"
  aspectRatio="widescreen"
>
  <p>This is a extremely detailed description of the product.</p>
  <Fragment slot="actions">
    <a class="bt bt-md bt-text " href="/details">
      View Details
    </a>
    <button class="bt bt-md bt-primary">Add to Cart</button>
  </Fragment>
</Card>
```

</li>
<li>

### Loading state

<div class="grid" col="1" col-md="2">
  <div class="grid_item">
    <Card imageSrc={placeholder} imageAlt="Product image" isLoading>
      <p>This is what the card looks like without the `isLoading` prop.</p>
    </Card>
  </div>
  <div class="grid_item">
    <Card imageSrc={placeholder} imageAlt="Product image">
      <p>This is what the card looks like without the `isLoading` prop.</p>
    </Card>
  </div>
</div>

```astro
---
import placeholder from "../../assets/images/image.png";
import Card from '../../components/Card.astro';
---
<Card imageSrc={placeholder} imageAlt="Product image" isLoading>
  <p>This is what the card looks like without the `isLoading` prop.</p>
</Card>
```

</li>
<li>

### Disabled state

<div class="grid" col="1" col-md="2">
  <div class="grid_item">
    <Card
      href="#"
      imageSrc={placeholder}
      imageAlt="Product image"
      isDisabled
    >
      <p>With the `isDisabled` prop, the link doesn't work anymore.</p>
    </Card>
  </div>
  <div class="grid_item">
    <Card href="#" imageSrc={placeholder} imageAlt="Product image">
      <p>Witout the `isDisabled` prop, the link is clickable.</p>
    </Card>
  </div>
</div>

```astro
---
import placeholder from "../../assets/images/image.png";
import Card from '../../components/Card.astro';
---
<Card
  href="/product/123"
  imageSrc={placeholder}
  imageAlt="Product image"
  isDisabled
>
  <p>With the `isDisabled` prop, the link doesn't work anymore.</p>
</Card>
```

</li>
<li>

### HTML example

<div class="grid" col="1" col-md="2">
  <div class="grid_item">
    <Card
      variant="raised"
      href="#"
      title="Product Name"
      subtitle="Category"
      imageSrc={placeholder}
      imageAlt="Product image"
      badges={[{ label: "new" }]}
    >
      <p>This is a extremely detailed description of the product.</p>
      <Fragment slot="actions">
        <a class="bt bt-text " href="#">
          View Details
        </a>
        <button class="bt bt-primary">Add to Cart</button>
      </Fragment>
    </Card>
  </div>
</div>

```html
<article class="card card-raised" style="--card-aspect-ratio: var(--ar-golden)">
  <header class="card_header">
    <div class="card_header_text">
      <h2 class="card_title">
        <a class="card_link" href="/product/123">Product Name</a>
      </h2>
      <p>Category</p>
    </div>
    <div class="card_badges"><span class="badge card_badge">new</span></div>
  </header>
  <figure class="card_media">
    <img
      class="card_image"
      src="image.jpg"
      alt="Product image"
      loading="lazy"
      decoding="async"
    />
  </figure>
  <section class="card_content">
    <p>This is a extremely detailed description of the product.</p>
  </section>
  <footer class="card_actions">
    <a class="bt bt-text" href="/details">View Details</a>
    <button class="bt bt-primary">Add to Cart</button>
  </footer>
</article>
```

</li>
</ul>
