Card

Raised content surface

Intent

Card is a raised surface that groups related content under one visual boundary: an article preview, a stat block, a feature card, a product row, a dashboard tile. It carries a background, border, radius, and shadow; everything inside inherits the card's rhythm from --card-gap.

Basic usage

A bare .card is enough. Drop a heading, body copy, and any supporting elements inside and they stack in the card's flex column with --card-gap spacing. Use an <article> element when the card's content stands alone (as most do); <div> is fine for purely decorative or non-document contexts.

Project milestones

Track progress across sprints. Update statuses on a weekly cadence, push blockers to the top, and let the team see where velocity pools.

<div class="demo-card-solo">
    <article class="card">
        <h3>Project milestones</h3>
        <p>Track progress across sprints. Update statuses on a weekly cadence, push blockers to the top, and let the team see where velocity pools.</p>
    </article>
</div>

Variants

Density

.is-compact tightens padding, gap, and font size one step. Use it when cards nest inside a denser surface -- a sidebar panel, a settings list, a data-heavy dashboard -- where the default padding would feel out of scale.

Chrome

Two modifiers tune the card's visual weight: .is-flat strips the background, border, and shadow for cards that sit on an already-raised surface (or for grouping content without visually framing it); .is-featured replaces the default border with a primary-colored stroke to pull a card out of a grid as the recommended choice.

Default

Background, border, radius, and a subtle shadow.

Compact

Tighter padding, gap, and font size.

Flat

No bg, border, or shadow. Sits on a parent surface.

<div class="card-grid">
    <article class="card">
        <h4>Default</h4>
        <p>Background, border, radius, and a subtle shadow.</p>
    </article>
    <article class="card is-compact">
        <h4>Compact</h4>
        <p>Tighter padding, gap, and font size.</p>
    </article>
    <article class="card is-flat">
        <h4>Flat</h4>
        <p>No bg, border, or shadow. Sits on a parent surface.</p>
    </article>
    <article class="card is-featured">
        <h4>Featured</h4>
        <p>Primary 2px border. Pulls out of a grid.</p>
    </article>
</div>

Regions

Card has two structural regions, both expressed by semantic HTML elements -- no slot classes. CSS targets the elements directly.

Media (<figure>)

A <figure> placed as the first child of the card receives a full-bleed treatment: negative margins equal to the card's padding pull it flush with the card's border, regardless of horizontal or vertical padding tokens. Use for a hero image, a chart preview, or any visual that should fill the card's width edge-to-edge. <img>, <picture>, and <video> all work inside the figure; <figcaption> is available when the image needs a caption.

A <figure> at any other position in the card stays at normal proportions (margins reset, no bleed), so a mid-card figure won't visually break the layout.

Launch plan

Handoff complete; staging deploy scheduled for Thursday.

<div class="demo-card-solo">
    <article class="card">
        <figure>
            <img src="https://picsum.photos/seed/card-figure/400/200" alt="" width="400" height="200"/>
        </figure>
        <h3>Launch plan</h3>
        <p>Handoff complete; staging deploy scheduled for Thursday.</p>
    </article>
</div>

The alt attribute is empty on the demo because the image is decorative. In production, describe the image meaningfully or leave alt="" if it's purely aesthetic -- a missing or boilerplate alt ("image", "photo") fails assistive tech.

A <footer> inside the card is pushed to the card's bottom edge via margin-block-start: auto. It carries no other styling defaults; compose what's inside the way the content wants. For an action row, a .cluster of buttons. For a byline, .text-muted on a paragraph or span.

Publish draft

Your draft is ready to go live. Review once more, then publish to the staging site for final checks before release.

<div class="demo-card-solo">
    <article class="card">
        <figure>
            <img src="https://picsum.photos/seed/card-footer/400/200" alt="" width="400" height="200"/>
        </figure>
        <h3>Publish draft</h3>
        <p>Your draft is ready to go live. Review once more, then publish to the staging site for final checks before release.</p>
        <footer class="cluster gap-snug">
            <button class="button is-outlined" type="button">Save for later</button>
            <button class="button" type="button">Publish</button>
        </footer>
    </article>
</div>

Composed patterns

Interactive card

When the whole card should act as a click target -- a gallery tile, an article preview, a dashboard item linking to a detail page -- add .is-interactive to the card root and put the actual <a> on the heading. Don't wrap the card itself in an anchor.

.is-interactive does two things. First, it styles the card as clickable: cursor, hover shadow, hover border. Second, it extends the heading anchor's click area over the whole card via an absolutely-positioned ::after that covers the card's bounds. The heading stays the single accessible link -- screen readers announce just the heading text as the link label -- and clicking anywhere on the card triggers the same link.

Q4 retro summary

Highlights, misses, and commitments from the quarterly review. Four action items assigned; owner dates in the linked doc.

Release checklist

Outstanding items before the 1.0 cut. Click the card anywhere to open the checklist; the action buttons keep their own click targets.

<docs-resize>
    <div class="card-grid" style="--card-grid-min: 20rem;">
        <article class="card is-interactive">
            <figure>
                <img src="https://picsum.photos/seed/card-interactive/400/200" alt="" width="400" height="200"/>
            </figure>
            <h3><a href="#">Q4 retro summary</a></h3>
            <p>Highlights, misses, and commitments from the quarterly review. Four action items assigned; owner dates in the linked doc.</p>
        </article>
        <article class="card is-interactive">
            <figure>
                <img src="https://picsum.photos/seed/card-interactive-footer/400/200" alt="" width="400" height="200"/>
            </figure>
            <h3><a href="#">Release checklist</a></h3>
            <p>Outstanding items before the 1.0 cut. Click the card anywhere to open the checklist; the action buttons keep their own click targets.</p>
            <footer class="cluster gap-snug">
                <button class="button is-outlined" type="button">Snooze</button>
                <button class="button" type="button">Mark complete</button>
            </footer>
        </article>
    </div>
</docs-resize>

Nested interactive elements inside .is-interactive: buttons and links inside the <footer> remain clickable -- the CSS gives the footer its own positioning context so its children paint above the stretched ::after. Any other nested interactive (a link inside the body, an icon-button overlay) needs position: relative on itself to escape the stretched layer. If the pattern you're building has several overlapping interactives, drop .is-interactive -- use the heading anchor as a normal link and let each nested control carry its own click target.

Card grid

Multiple cards lay out in a responsive grid with .card-grid: auto-fill columns with a minimum width set by --card-grid-min. Cards in the same row stretch to equal height, and each card's footer pins to its bottom edge, so the plain grid stays tidy even when cards have different regions — some with a figure, some without, some with no footer. For most runs of cards this is all you need.

Access control audit

Review role assignments and stale invitations before the security review on Friday.

Rollout checklist

Staged ship plan with go/no-go criteria for each phase. No figure on this card; the heading starts at the top and the footer still pins to the bottom edge.

Docs migration

No figure, no footer. The card still stretches to match the row's height.

<docs-resize>
    <div class="card-grid">
        <article class="card">
            <figure>
                <img src="https://picsum.photos/seed/plain-1/400/180" alt="" width="400" height="180"/>
            </figure>
            <h4>Access control audit</h4>
            <p>Review role assignments and stale invitations before the security review on Friday.</p>
            <footer class="cluster gap-snug">
                <button class="button is-outlined" type="button">Open</button>
            </footer>
        </article>
        <article class="card">
            <h4>Rollout checklist</h4>
            <p>Staged ship plan with go/no-go criteria for each phase. No figure on this card; the heading starts at the top and the footer still pins to the bottom edge.</p>
            <footer class="cluster gap-snug">
                <button class="button" type="button">Continue</button>
            </footer>
        </article>
        <article class="card">
            <h4>Docs migration</h4>
            <p>No figure, no footer. The card still stretches to match the row&#39;s height.</p>
        </article>
    </div>
</docs-resize>

Aligned card grid

Add .is-aligned when you also want the regions inside the cards to align across the row: every title, body block, and footer starting at the same height in each sibling. Cards switch to grid-template-rows: subgrid, inheriting rows whose heights are set by the tallest content in each position — alignment without per-card measuring.

Body-length differences alone won't show the effect: the footer's auto margin already absorbs those in the plain grid. The difference appears when a region above another varies in height. The demo below renders the same three cards twice; the first title is long enough to wrap. In the plain grid, the wrapped title pushes only its own card's body down. With .is-aligned, the heading band is sized by the tallest title, so every body starts on the same line. Resize to see the wrap point move.

Plain .card-grid — the wrapped title pushes its own body down

Access control and stale invitation audit in a project

Review role assignments before Friday.

Rollout checklist

Staged ship plan with go/no-go criteria for each phase.

Docs migration

Move remaining pages from the old CMS.

With .is-aligned — every body starts below the tallest title

Access control and stale invitation audit in a project

Review role assignments before Friday.

Rollout checklist

Staged ship plan with go/no-go criteria for each phase.

Docs migration

Move remaining pages from the old CMS.

<docs-resize>
    <div class="flow">
        <p class="text-muted text--1">Plain <code>.card-grid</code> — the wrapped title pushes its own body down</p>
        <div class="card-grid">
            <article class="card">
                <h4>Access control and stale invitation audit in a project</h4>
                <p>Review role assignments before Friday.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
            <article class="card">
                <h4>Rollout checklist</h4>
                <p>Staged ship plan with go/no-go criteria for each phase.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
            <article class="card">
                <h4>Docs migration</h4>
                <p>Move remaining pages from the old CMS.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
        </div>
        <p class="text-muted text--1">With <code>.is-aligned</code> — every body starts below the tallest title</p>
        <div class="card-grid is-aligned" style="--card-grid-rows: 3">
            <article class="card">
                <h4>Access control and stale invitation audit in a project</h4>
                <p>Review role assignments before Friday.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
            <article class="card">
                <h4>Rollout checklist</h4>
                <p>Staged ship plan with go/no-go criteria for each phase.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
            <article class="card">
                <h4>Docs migration</h4>
                <p>Move remaining pages from the old CMS.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
        </div>
    </div>
</docs-resize>

The same comparison with full cards: figure, heading, body, footer. This is the structure the default --card-grid-rows of 4 assumes, so .is-aligned needs no override here. The figures share an aspect ratio and contribute no height difference; the wrapped title is again what moves the bands.

Plain .card-grid

Access control and stale invitation audit in a project

Review role assignments before the security review on Friday.

Rollout checklist

Staged ship plan with go/no-go criteria for each phase. Sign-off required before each window.

Docs migration

Move remaining pages from the old CMS; redirect map drafted.

With .is-aligned

Access control and stale invitation audit in a project

Review role assignments before the security review on Friday.

Rollout checklist

Staged ship plan with go/no-go criteria for each phase. Sign-off required before each window.

Docs migration

Move remaining pages from the old CMS; redirect map drafted.

<docs-resize>
    <div class="flow">
        <p class="text-muted text--1">Plain <code>.card-grid</code></p>
        <div class="card-grid">
            <article class="card">
                <figure>
                    <img src="https://picsum.photos/seed/grid-1/400/180" alt="" width="400" height="180"/>
                </figure>
                <h4>Access control and stale invitation audit in a project</h4>
                <p>Review role assignments before the security review on Friday.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
            <article class="card">
                <figure>
                    <img src="https://picsum.photos/seed/grid-2/400/180" alt="" width="400" height="180"/>
                </figure>
                <h4>Rollout checklist</h4>
                <p>Staged ship plan with go/no-go criteria for each phase. Sign-off required before each window.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                    <button class="button" type="button">Continue</button>
                </footer>
            </article>
            <article class="card">
                <figure>
                    <img src="https://picsum.photos/seed/grid-3/400/180" alt="" width="400" height="180"/>
                </figure>
                <h4>Docs migration</h4>
                <p>Move remaining pages from the old CMS; redirect map drafted.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
        </div>
        <p class="text-muted text--1">With <code>.is-aligned</code></p>
        <div class="card-grid is-aligned">
            <article class="card">
                <figure>
                    <img src="https://picsum.photos/seed/grid-1/400/180" alt="" width="400" height="180"/>
                </figure>
                <h4>Access control and stale invitation audit in a project</h4>
                <p>Review role assignments before the security review on Friday.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
            <article class="card">
                <figure>
                    <img src="https://picsum.photos/seed/grid-2/400/180" alt="" width="400" height="180"/>
                </figure>
                <h4>Rollout checklist</h4>
                <p>Staged ship plan with go/no-go criteria for each phase. Sign-off required before each window.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                    <button class="button" type="button">Continue</button>
                </footer>
            </article>
            <article class="card">
                <figure>
                    <img src="https://picsum.photos/seed/grid-3/400/180" alt="" width="400" height="180"/>
                </figure>
                <h4>Docs migration</h4>
                <p>Move remaining pages from the old CMS; redirect map drafted.</p>
                <footer class="cluster gap-snug">
                    <button class="button is-outlined" type="button">Open</button>
                </footer>
            </article>
        </div>
    </div>
</docs-resize>

.is-aligned requires uniform cards: subgrid placement is positional, so every card must have the same regions in the same order. A card missing its figure would put its heading in the figure-height row. The default row count assumes figure, heading, body, and footer; adjust --card-grid-rows if your cards share a different count. For mixed-structure cards, use the plain .card-grid.

Reference

  • .card -- the raised surface. Render as <article> for standalone content, <div> for non-document contexts.
  • .card-grid -- auto-fill grid parent; equal-height cards per row.
  • .card-grid.is-aligned -- adds subgrid row alignment across uniform cards. See Aligned card grid.
  • <figure> (first child of .card) -- full-bleed media slot. <img>, <picture>, <video>, and <figcaption> all work inside. Slot keyed by element. See Media.
  • <footer> (inside .card) -- pushed to the card's bottom edge. No styling defaults; compose .cluster + buttons, or .text-muted for byline. Slot keyed by element. See Footer.
  • .is-compact -- density variant.
  • .is-flat -- transparent chrome variant.
  • .is-featured -- primary border variant.
  • .is-interactive -- clickable-card styling plus stretched-link behavior: the card gets hover shadow and cursor, and the heading's <a> extends its hit area over the whole card via an absolutely-positioned ::after. Put the anchor on the heading; don't wrap the card itself in <a>.

Customization

Card exposes scoped tokens for color (background, border, shadow), sizing (padding block/inline, radius, gap), typography (heading size, content font size), and the figure slot (radius, optional explicit height). .card-grid adds two of its own: a minimum column width and, for .is-aligned, a subgrid row span. Density and chrome variants remap the card tokens; the figure's negative margins read --card-padding-inline and --card-padding-block, so altering the padding flows through to the full-bleed effect automatically.

Full token list and defaults: src/css/card.css.

Accessibility

  • Heading level matches the document. The heading inside a card is a real heading in the document outline. Pick the level that reflects the card's place in the page's hierarchy -- don't default to h3 because it looks right. Card's --card-heading-size normalizes visual size, so any level reads consistently.
  • Alt text discipline on media. An empty alt="" is correct when the image is purely decorative (as in these demos). A meaningful alt is required when the image carries information not present in surrounding text. Never leave alt off entirely, and avoid placeholder words like "image" or "photo".
  • Link the heading, not the card. .is-interactive extends the heading anchor's hit area over the whole card, so the click target is wide without sacrificing the short, scannable link name screen readers announce. Wrapping the card itself in <a> would make the link's accessible name the card's entire text content and forbid any nested interactive element -- both regressions over linking the heading.
  • Nested interactives with .is-interactive. Buttons and links inside <footer> paint above the stretched layer and work as expected. Other nested interactives (a link in the body, an icon-button overlay) need position: relative on themselves to escape the stretched layer. If your card has many overlapping interactive elements, don't use .is-interactive -- keep the heading anchor as a plain link and let each nested control carry its own click target.

CSS Reference: Card

Elevated container for grouped content

Source: src/css/card.css

Tokens

TokenDefaultDescription
--card-bgvar(--color-surface-raised)Background color for the card
--card-bordervar(--color-border)Border color for the card
--card-radiusvar(--radius-md)Border radius for the card corners
--card-padding-blockvar(--ui-pad-y-relaxed)Vertical padding inside the card
--card-padding-inlinevar(--ui-pad-x-relaxed)Horizontal padding inside the card
--card-shadowvar(--shadow-sm)Box shadow for the card
--card-gapvar(--gap-relaxed)Vertical spacing between card regions
--card-heading-sizevar(--step-1)Font size for the card heading
--card-content-font-sizeinheritFont size for the card body content
--card-media-radiusvar(--radius-md)Border radius for the first-child media figure
--card-media-heightautoFixed height for the media image or video

Slots

SlotDescription
> figure:first-childFull-bleed slot when figure is the first child
> footerThe footer slot is pushed to the bottom edge

Classes

ClassDescription
.cardcomponent root
.is-compactTighter padding, gap, and smaller heading
.is-flatNo shadow, border, or background
.is-interactiveHover shadow and stretched heading link for whole-card click
.is-featuredPrimary-colored thick border for emphasis

CSS Reference: Card Grid

Auto-fill grid of equal-height cards

Source: src/css/card.css

Tokens

TokenDefaultDescription
--card-grid-min18rem), 1frMinimum column width for auto-fill columns
--card-grid-rows4Number of subgrid rows each card spans

Classes

ClassDescription
.card-gridcomponent root
.card-grid.is-alignedSubgrid row alignment across uniform cards