Skip to content

ExpressiveCSS

Browse documentation

Cards

Material Design 3 cards, from the HTML.

View as Markdown

Use an <article> for an elevated card. Inside it, any heading is the headline, <p class="subhead"> is the optional subhead, and a <p> is supporting copy. A direct <img>, <picture>, or <figure> is media, and a 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. The elevated container is surface-container-low with 12dp corners. The default sits at elevation 1, and an interactive card rises to 2 on hover. The headline is title-medium in on-surface, the subhead is title-small in on-surface, and supporting text is body-medium in on-surface-variant. Inset is 16dp.

Weekend in the mountains

Three-day itinerary

Explore trails, overlooks, and places to rest along the way.

Mountain valley beneath a cloudy sky
<article>
  <header>
    <h3>Weekend in the mountains</h3>
    <p class="subhead">Three-day itinerary</p>
  </header>
  <p>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

Cards are elevated by default. Add filled for surface-container-highest at rest, with no shadow. Add outlined (or border) for a 1dp outline-variant stroke over surface.

Elevated

Surface-container-low, elevation 1.

Filled

Surface-container-highest, no elevation.

Outlined

Surface plus a 1dp outline.

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

Primary action and states

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 static card has no hover treatment. 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

Add card-collection to a container for 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. When the content needs a different column strategy, change the minimum card width or replace the complete grid track token.

Pho

$12

Burrito Bowl

$9

Quinoa Salad

$10

List

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

Pho

Rice noodles, herbs, and aromatic broth · $12

Quinoa Salad

Roasted vegetables, herbs, and lemon dressing · $10

Staggered grid

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

Pho

$12

Combo #2

Two entrées, three sides, and two drinks · $28

Pumpkin Soup

$8

Combo #1

Entrée, side, and drink · $14

Mosaic grid

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

Pho

$12

Combo: Entrée, side, and drink

$14

Combo #2: 2 entrées, 3 sides, and 2 drinks

$28

Quinoa Salad

$10

Carousel

Add carousel uncontained to the collection and 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. To caption the image, wrap it in a <figure>. The <figcaption> is an opaque, rounded bounding shape with the complete title-medium type role, including letter-spacing. Its tracking follows the title-medium token and can affect text width or wrapping. The shape uses the paired surface and on-surface roles, so image colors cannot reduce the contrast of its text or icons. Normal text must keep at least 4.5:1 contrast. Large text and meaningful icons need at least 3:1. Recheck those ratios if you override either color token.

Card title

I am a very simple card. I am good at containing small bits of information.

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

For an icon that is not a Material Symbol, such as an inline <svg> or an image, add card-icon. It sizes the icon to --md-comp-card-icon-size (24px) and colors it with --md-comp-card-icon-color (the primary role). Inside a <figcaption> it takes the caption's text color instead.

Horizontal

Add horizontal to move the media to the start and stack the headline, supporting text, and action beside it. The content order stays the same in both orientations. When the whole horizontal card is one destination, a direct .primary-action link can wrap those same slots.

Without a size helper, the card's height follows content with a 240px minimum. small, medium, and large set fixed expanded heights. Below 600px, both forms return to the vertical layout and fixed heights reset to content. Inside a .query-container narrower than 600px they do the same on any window, so a horizontal card in a sidebar stacks too.

Musician playing guitar during a live performance

Performances at The Hideout

Watch exclusive live performances at The Hideout every Saturday starting at 7pm.

Musician playing guitar during a live performance

Performances at The Hideout

Watch exclusive live performances at The Hideout every Saturday starting at 7pm.

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

Put the details in an <aside>. It expands in normal flow below the persistent media, headline, and subhead. Place a .card-reveal-trigger button over the media, and use that same native button to open and close the panel. The reveal grows the card instead of covering or internally scrolling it. This replaces legacy .activator markup and heading-based close controls.

Cards.Init(), and AutoInit() when the card contains both that button and one identified direct <aside>, manages aria-expanded. It applies the closed-panel styles only when the disclosure markup passes these checks. The trigger must be enabled when Cards initializes. A disclosure that fails, such as one with only disabled triggers, an unidentified panel, or several panels, leaves 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.

Cards.init(el, options) takes two callbacks, both null by default and both called with the card: onOpen after the panel opens and onClose after it closes. open() and close() on the instance do the same as the trigger.

<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

Use an <article class="expanding-card"> with a direct <dialog class="expanding-card-dialog">. The card performs a shared-container transition from a compact feed item into a full-screen modal detail surface. 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.

The container morph and the hero run on the slow spatial spring. The close button and detail content run on the default spatial spring, and their fade on the fast effects spring. So motion="expressive" reaches all three. To change container timing, override --md-comp-expanding-card-motion-duration and --md-comp-expanding-card-motion-easing together. A spring easing over another duration is no longer that spring.

Detail content starts after --md-comp-expanding-card-content-delay, which reads --md-sys-motion-duration-short4 and defaults to 200ms. Override the system token on the page, or set the component property on the article to change this card's delay. A value of 0ms removes the delay. Reduced motion disables the transitions even with a nonzero delay override.

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. ExpandingCard.init(el, options) takes two callbacks, both called with the card. onOpen runs once the dialog is open and focus is on the back button. onClose runs when closing starts.

Plants in patterned vases against a blue wall

Listen to Glass Souls

From your recent favorites

Plants in patterned vases against a blue wall

Glass Souls’ Biggest Hits

Fragile Glass Souls
<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%, and the action row sticks to the bottom. These sizes are optional layout helpers and are not part of the M3 spec.

Small

The small card is 300px tall.

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

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow