# Cards

> Material Design 3 cards, from the HTML.

## Anatomy

An `<article>` is an elevated card. Any heading is the headline, `<p class="subhead">` is the optional subhead, `<p class="supporting-text">` is supporting copy, direct `<img>`, `<picture>`, or `<figure>` is media, and direct `.actions` is the action row. Include only the slots the content needs. There is no `card-content`, `card-title`, or `card-action` class.

Tokens follow the [M3 card spec](https://m3.material.io/components/cards/specs). The elevated container is `surface-container-low` with 12dp corners. The default sits at elevation 1; an interactive card rises to 2 on hover. The headline is `title-medium` / `on-surface`, the subhead is `title-small` / `on-surface`, and supporting text is `body-medium` / `on-surface-variant`. Inset is 16dp.

```
<article>
  <header>
    <h3>Weekend in the mountains</h3>
    <p class="subhead">Three-day itinerary</p>
  </header>
  <p class="supporting-text">Explore trails and overlooks.</p>
  <img src="images/mountains.jpg" alt="Mountain valley beneath a cloudy sky">
  <div class="actions">
    <button type="button" class="text">Share</button>
    <button type="button" class="tonal">View trip</button>
  </div>
</article>
```

## Variants

Default is elevated. `filled` uses `surface-container-highest` at rest (no shadow). `outlined` (or `border`) draws a 1dp `outline-variant` stroke over `surface`.

```
<article>…</article>
<article class="filled">…</article>
<article class="outlined">…</article>
```

## Primary action and states

A static card has no hover treatment. To make the card an entry point, wrap its primary content in a direct `<a class="primary-action">`. That gives the card M3 hover, focus, pressed, and focus-indicator states. A directly actionable card has no other links or buttons. When the content needs several actions, leave the card static and put the controls in `.actions`. Toggle `dragged` or `picked-up` during reordering; that state keeps its dragged elevation and 16% state layer even if the primary action remains hovered or pressed. For a disabled destination, remove `href` and set both `aria-disabled="true"` and `tabindex="-1"` on the primary action.

```
<article>
  <a class="primary-action" href="/reservation/42">
    <h3>Upcoming reservation</h3>
    <p>Open the reservation details.</p>
  </a>
</article>
```

## Collections

`card-collection` creates a responsive grid with no more than 8dp between cards. Cards keep their variant-specific resting elevation: level 1 for elevated, level 0 for filled and outlined. Add `picked-up` or `dragged` only while a card is being moved. Add `list`, `staggered`, `mosaic`, or `carousel uncontained` for the other collection layouts. Sorting and filtering controls stay outside the collection.

### Responsive grid

The default uses auto-fit columns. Change the minimum card width or replace the complete grid track token when the content needs a different column strategy.

### List

`list` keeps one card per row and 8dp spacing without changing each card's variant-specific resting elevation.

### Staggered grid

`staggered` packs intrinsic card heights into masonry-like columns. Source order flows down each column.

### Mosaic grid

`mosaic` uses dense grid placement. Set column and row spans on individual cards from application data.

### Carousel

Add `carousel uncontained`. Add `carousel-item` to every card; `AutoInit()` supplies the M3 scroll track.

```
<!-- Responsive grid (the default). Override the track token as needed. -->
<section class="card-collection" aria-label="Dinner menu">
  <article class="outlined">…</article>
  <article class="outlined">…</article>
</section>

<!-- List -->
<section class="card-collection list" aria-label="Dinner menu">
  <article class="outlined"><h3>Pho</h3><p>$12</p></article>
  <article class="outlined"><h3>Quinoa Salad</h3><p>$10</p></article>
</section>

<!-- Intrinsic-height staggered grid -->
<section class="card-collection staggered" aria-label="Dinner menu">
  <article class="outlined"><h3>Pho</h3><p>$12</p></article>
  <article class="outlined"><h3>Combo #2</h3><p>Two entrées, three sides, and two drinks · $28</p></article>
</section>

<!-- Dense mosaic. Set spans per card from application data. -->
<section class="card-collection mosaic" aria-label="Dinner menu">
  <article style="--md-comp-card-collection-row-span: 3">…</article>
  <article style="--md-comp-card-collection-column-span: 2; --md-comp-card-collection-row-span: 4">…</article>
</section>

<!-- M3 uncontained carousel. AutoInit starts the Carousel component. -->
<section class="card-collection carousel uncontained" aria-label="Dinner menu">
  <article class="outlined carousel-item"><h3>Pho</h3><p>$12</p></article>
  <article class="outlined carousel-item"><h3>Quinoa Salad</h3><p>$10</p></article>
</section>
```

## Media

A direct `<img>` is full-bleed across the top and its media surface has rounded corners matching the card. Wrap it in a `<figure>` if you want a caption on the image. The `<figcaption>` is an opaque, rounded bounding shape using the paired `surface` and `on-surface` roles, so image colors cannot reduce the contrast of its text or icons. Normal text must retain at least 4.5:1 contrast; large text and meaningful icons require at least 3:1. Recheck those ratios if you override either color token.

```
<article>
  <figure>
    <img src="images/sample-1.jpg" alt="">
    <figcaption>
      <span class="material-symbols" aria-hidden="true">landscape</span>
      <span>Card title</span>
    </figcaption>
  </figure>
  <p>I am a very simple card.</p>
  <div class="actions">
                <button type="button" class="text">Action</button>
  </div>
</article>
```

## Horizontal

The same card can use two orientations without changing its content order. Add `horizontal` to move the media to the start and stack the headline, supporting text, and action beside it. A direct `.primary-action` link can wrap those same slots when the whole horizontal card is one destination. Without a size helper, its height follows content with a 240px minimum; `small`, `medium`, and `large` provide fixed expanded heights. Below 600px, both forms return to the vertical layout and fixed heights reset to content.

```
<!-- Vertical -->
<article class="outlined">
  <img src="images/the-hideout.jpg" alt="Musician playing guitar during a live performance">
  <h3>Performances at The Hideout</h3>
  <p>Watch exclusive live performances at The Hideout every Saturday starting at 7pm.</p>
  <div class="actions">
    <button type="button" class="tonal">Get tickets</button>
  </div>
</article>

<!-- Horizontal: only the orientation class changes. -->
<article class="outlined horizontal">
  <img src="images/the-hideout.jpg" alt="Musician playing guitar during a live performance">
  <h3>Performances at The Hideout</h3>
  <p>Watch exclusive live performances at The Hideout every Saturday starting at 7pm.</p>
  <div class="actions">
    <button type="button" class="tonal">Get tickets</button>
  </div>
</article>

<!-- Directly actionable horizontal card. -->
<article class="outlined horizontal">
  <a class="primary-action" href="/performances/the-hideout">
    <img src="images/the-hideout.jpg" alt="Musician playing guitar during a live performance">
    <h3>Performances at The Hideout</h3>
    <p>Watch exclusive live performances every Saturday.</p>
  </a>
</article>
```

## Reveal

An `<aside>` expands in normal flow below the persistent media, headline, and subhead. Place a `.card-reveal-trigger` button over the media; the same button opens and closes the details. `Cards.Init()` (and `AutoInit()` when the card contains both that button and one identified direct `<aside>`) owns `aria-expanded` and applies the closed-panel styles only after the disclosure contract is accepted. The trigger must be enabled when Cards initializes; rejected, disabled-only, unidentified, or multi-panel disclosures leave every panel visible, available, and untouched. The native button handles Enter and Space. Escape closes an open panel and returns focus to its trigger. A trigger belongs to its closest `<article>`; nested cards, including cards inside an outer reveal panel, keep their own disclosure ownership and do not initialize or toggle the outer card. The reveal grows the card instead of covering or internally scrolling it. This replaces legacy `.activator` markup and heading-based close controls. Use the same native button to open and close the panel.

```
<article class="filled">
  <figure>
    <img src="images/ana-russo.jpg" alt="Portrait of Ana Russo">
    <button type="button"
            class="card-reveal-trigger"
            aria-label="Toggle contact details"
            aria-controls="ana-contact"></button>
  </figure>
  <header class="card-reveal-summary">
    <h3>Ana Russo</h3>
    <p class="subhead">Sibling</p>
  </header>
  <aside id="ana-contact">
    <address class="reveal-actions">
      <a class="reveal-action" href="tel:+16505551234">
        <span class="material-symbols" aria-hidden="true">call</span>
        <span>(650) 555-1234</span>
      </a>
      <a class="reveal-action" href="mailto:hey@anarusso.com">
        <span class="material-symbols" aria-hidden="true">mail</span>
        <span>hey@anarusso.com</span>
      </a>
    </address>
  </aside>
</article>
```

## Expanding card

An expanding card performs a shared-container transition from a compact feed item into a full-screen modal detail surface. Use an `<article class="expanding-card">` with a direct `<dialog class="expanding-card-dialog">`. Keep the same hero image in both states so the media appears to grow with the container. `ExpandingCard.Init()` and `AutoInit()` wire up the modal, measured clip origin, back action, focus return, Escape, and reduced motion.

Override `--md-comp-expanding-card-motion-duration` to change container timing. Closing waits for the actual clip transition, including cancellation; zero duration and reduced motion close immediately. Reopening or destroying the instance invalidates pending close cleanup. The `onClose` callback runs when closing starts.

```
<article class="outlined expanding-card">
  <figure>
    <img src="images/glass-souls.jpg" alt="Pastel balloons floating above flowers">
    <button type="button" class="expanding-card-trigger"
            aria-label="Open Glass Souls album" aria-haspopup="dialog"></button>
  </figure>
  <header class="expanding-card-summary">
    <h3>Listen to Glass Souls</h3>
    <p class="subhead">From your recent favorites</p>
  </header>
  <dialog id="glass-souls-card" class="expanding-card-dialog"
          aria-labelledby="glass-souls-title">
    <button type="button" class="expanding-card-close" aria-label="Back">
      <span class="material-symbols" aria-hidden="true">arrow_back</span>
    </button>
    <figure class="expanding-card-hero">
      <img src="images/glass-souls.jpg" alt="Pastel balloons floating above flowers">
    </figure>
    <div class="expanding-card-content">
      <header class="expanding-card-detail-header">
        <h2 id="glass-souls-title">Glass Souls’ Biggest Hits</h2>
        <div class="expanding-card-actions">
          <button type="button" class="expanding-card-favorite" aria-label="Favorite album">
            <span class="material-symbols" aria-hidden="true">favorite</span>
          </button>
          <button type="button" class="expanding-card-play" aria-label="Play album">
            <span class="material-symbols" aria-hidden="true">play_arrow</span>
          </button>
        </div>
      </header>
      <div class="expanding-card-track">
        <strong>Fragile</strong><small>Glass Souls</small>
        <time datetime="PT3M34S">3:34</time>
      </div>
    </div>
  </dialog>
</article>
```

## Sizes

`small`, `medium`, and `large` lock the height at 300px, 400px, and 500px so a row of cards lines up. Media takes the top 60%; the action row sticks to the bottom. These sizes are not in the M3 spec — they are optional layout helpers.

```
<article class="small">…</article>
<article class="medium">…</article>
<article class="large">…</article>
```
