# 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.

Make sure you check out the [Tiles](/components/tiles) component to see how to lay cards out in equal-height, container-responsive columns. (See [below](/components/card#blog-index-pattern) and [this site's own blog index](/blog) for examples.)

<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] |
| `Card.astro`                          | The Astro component                            | [Github][3] |
| `Tags.astro`, `Avatar.astro`          | Composed components (dependencies)             | [Github][3] |
| [Tiles](/components/tiles)            | Equal-height, container-responsive list layout | [Github][2] |
| `--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.tiles.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>SR</span></div></div><div><span class="card_author">Sonny Rollins</span><time datetime="2026-05-02">May 2, 2026</time></div></footer>',
      code: '<footer class="card_byline"><div class="avatar avatar-sm"><div><span>SR</span></div></div><div><span class="card_author">Sonny Rollins</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, on a [Tiles](/components/tiles) list. Tiles is container-responsive: columns appear as the column the list sits in crosses the size's container thresholds, so this site's blog index gets its 4-up laptop layout purely by widening the page column (`--layout-content-width`), no per-list tuning:

```html
<ul class="tiles">
  <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 Rollins</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>

**Tiles**

Lists of cards go in a [Tiles](/components/tiles) component: a `ul` whose items stretch, so cards in a row share the same height, with a container-responsive column count. See its page for the sizes, thresholds, and container rules.

### 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

<Tiles>
  <li>
    <Card
      href="#"
      headingLevel={3}
      flushImage
      imageSrc={placeholder.src}
      title="Designing with cascade layers"
      tags={["css", "learn"]}
      author="Sonny Rollins"
      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>
</Tiles>

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

</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>
