Navigation

v0.0.4
Materials

Card

A raised sheet whose parts follow the room the card is given.

On this page

A card is a raised sheet with a small structure: media, a header, a body and a footer of actions. Use .card on an <article>, a <section> or a list item. The card is a size container, so its parts follow the room the card is given rather than the viewport: the same markup works in a sidebar, a grid and a full-width column.

Card

<article class="card max-w(320) mx-auto">  <img class="card-media" src="/demo/moonstone.svg" alt>  <header class="card-header">    <h3 class="card-title">Atlas</h3>    <p class="card-description">atlas.dev · Deployed 2 hours ago</p>    <span class="badge badge-success badge-dot card-action">Ready</span>  </header>  <footer class="card-footer">    <button type="button" class="btn btn-sm">Logs</button>    <button type="button" class="btn btn-sm btn-primary">Visit</button>  </footer></article>

#Parts

Every part is optional, and the card spaces whichever are present by 1rem.

  • card-media is an image or a video at the top. It sits 4px inside the card, with a corner 4px smaller than the card's so the two stay concentric, and takes a faint inward outline so a pale image keeps its edge.
  • card-header holds a card-title, a card-description and, at its end, a card-action: a status, a menu button or a switch.
  • card-body is free content.
  • card-footer lines up actions at the end, the main one last.

Parts

<section class="card max-w(380) mx-auto">  <header class="card-header">    <h3 class="card-title">Preview deployments</h3>    <p class="card-description">Every pull request gets its own URL.</p>    <input type="checkbox" role="switch" class="switch card-action" checked aria-label="Preview deployments">  </header>  <div class="card-body">    <label class="field">Branch pattern      <input class="input" value="feature/*">    </label>  </div>  <footer class="card-footer">    <button type="button" class="btn btn-sm">Reset</button>    <button type="button" class="btn btn-sm btn-primary">Save</button>  </footer></section>

#It follows its container

The card declares container: card / inline-size and its parts query it. Queries measure the card's content box, inside its padding:

  • Under 15rem, the card-action moves below the title and description, and the footer's buttons stack at full width with the main one first.
  • card-split sets the media beside the content from 28rem, 4px inside the card on three sides, and stacks it on top below that.

Drag the handle to narrow the preview: the layout changes with the card, not with the window.

Split card

656 px
<article class="card card-split">  <img class="card-media" src="/demo/moonstone.svg" alt>  <header class="card-header">    <h3 class="card-title">Moonstone and obsidian</h3>    <p class="card-description">One material language in light and dark.</p>    <span class="badge card-action">Guide</span>  </header>  <div class="card-body">Every surface is a step on one ladder, lit from above. Edges are half-pixel contours and short shadows that fade.</div>  <footer class="card-footer">    <button type="button" class="btn btn-sm">Later</button>    <button type="button" class="btn btn-sm btn-primary">Read the guide</button>  </footer></article>

Because the card is a size container, its width must come from its context: a grid track, a flex item with a basis, or a block. In a shrink-to-fit context such as inline-flex, give it a width.

#Flat

card-flat keeps one contour and no lift, for cards in a dense list or on a tray.

Flat

<div class="tray grid grid-cols(2) gap(4)">  <article class="card card-flat">    <header class="card-header">      <h3 class="card-title">Requests</h3>      <p class="card-description">Last 24 hours</p>    </header>    <div class="card-body text-md fg-text-strong">1.28M</div>  </article>  <article class="card card-flat">    <header class="card-header">      <h3 class="card-title">Errors</h3>      <p class="card-description">Last 24 hours</p>    </header>    <div class="card-body text-md fg-text-strong">0.02%</div>  </article></div>

#Interactive

When the whole card is one link or one button, put card and card-interactive on the <a> or <button> itself. It lifts under the pointer and settles when pressed, as a button does. Do not nest other controls inside it.

Interactive

<a class="card card-interactive" href="#installation">  <span class="card-header">    <span class="card-title">Installation</span>    <span class="card-description">Add Master UI to a project.</span>  </span></a><a class="card card-interactive" href="#theme">  <span class="card-header">    <span class="card-title">Theme</span>    <span class="card-description">Materials, light and tokens.</span>  </span></a>

#Classes

ClassDescription
cardThe raised sheet and size container; its parts are spaced by 1rem
card-flatOne contour and no shadow, for cards in a dense list or on a tray. With card
card-interactiveA card that is one link or button: it lifts under the pointer and sinks when pressed. No other control goes inside it. With card
card-splitSets the media beside the content from 28rem of content width, and on top below that. With card
card-mediaAn image or video at the top, 4px inside the card, 16 / 9 by default
card-headerThe heading group: a title, a description and an optional action at the end
card-titleThe title, in the header
card-descriptionA muted line under the title, in the header
card-actionA control or a status at the end of the header; under 15rem of content it moves below the text
card-bodyFree content
card-footerActions at the foot, aligned to the end with the main one last; under 15rem of content they stack at full width, the main one first

#Custom properties

PropertyDefaultDescription
--card-padding1.25remPadding around the parts
--card-radiusvar(--radius-surface)Corner radius
--card-media-inset4pxSpace between the media and the card's edge
--card-split.4Share of the width the media takes in a split card. On card-split

#Source

@master/ui/components/card.css
/* * Card: a raised sheet with a small structure: media, a header, a body and a footer of actions. * The card is a size container, so its parts follow the room the card is given, not the viewport: the header's * action drops under the title and the footer's buttons stack in a narrow card, and a split card sets its * media beside its content once it is wide enough. Queries measure the card's content box, inside its padding. * Media sits 4px inside, and its corner is the card's corner minus that inset. */@layer components {  .card {    --card-padding: 1.25rem;    --card-radius: var(--radius-surface);    --card-media-inset: 4px;    --surface-color: var(--color-surface-raised);    container: card / inline-size;    position: relative;    display: flex;    flex-direction: column;    gap: 1rem;    min-width: 0;    padding: var(--card-padding);    border-radius: calc(var(--card-radius) * var(--corner-scale));    corner-shape: var(--corner-shape);    @apply --surface-sheet;    color: var(--color-text-body);    box-shadow: var(--shadow-lifted);    font-size: .875rem;    line-height: 1.55;    text-decoration: none;    /* One contour and no lift, for cards in a dense list or on a tray. */    &.card-flat {      background: var(--color-surface-raised);      box-shadow: var(--shadow-flat);    }    /* A card that is one link or button: it lifts under the pointer and sinks when pressed, as a button does. */    &.card-interactive {      --card-lift: calc(-2 * var(--lift));      -webkit-tap-highlight-color: transparent;      cursor: pointer;      transition-property: box-shadow, translate;      transition-duration: var(--duration-quick);      transition-timing-function: var(--easing-glide);      @media (hover: hover) {        &:hover {          translate: 0 var(--card-lift);          box-shadow: var(--shadow-floating);        }      }      &:active {        translate: 0 0;        box-shadow: var(--shadow-raised);        transition-duration: var(--duration-press);      }      &:focus-visible {        @apply --focus-ring;      }      @media (prefers-reduced-motion: reduce) {        --card-lift: 0px;      }    }    @media (forced-colors: active) {      outline: 1px solid CanvasText;      outline-offset: -1px;    }  }  /* An image or video at the top of the card, finished with a faint inward outline so pale pixels keep an edge. */  .card-media {    display: block;    flex: none;    width: calc(100% + (var(--card-padding) - var(--card-media-inset)) * 2);    max-width: none;    margin: calc(var(--card-media-inset) - var(--card-padding));    margin-bottom: 0;    border-radius: calc(var(--card-radius) * var(--corner-scale) - var(--card-media-inset));    corner-shape: var(--corner-shape);    aspect-ratio: 16 / 9;    object-fit: cover;    background: var(--color-surface-chip);    outline: 1px solid var(--color-line-image);    outline-offset: -1px;  }  /* The title and its description, with an optional action at the end of the first line. */  .card-header {    display: grid;    grid-template-columns: minmax(0, 1fr) auto;    gap: .25rem 1rem;    align-items: start;  }  .card-title {    grid-column: 1;    margin: 0;    color: var(--color-text-strong);    font: var(--font-weight-control) .9375rem/1.4 var(--font-family-sans);    font-feature-settings: inherit;    text-wrap: balance;    overflow-wrap: anywhere;  }  .card-description {    grid-column: 1;    margin: 0;    color: var(--color-text-muted);    font-size: .8125rem;    text-wrap: pretty;    overflow-wrap: anywhere;  }  /* A control or a status at the end of the header. Under 15rem of content it moves below the text it belongs to. */  .card-action {    grid-row: 1 / span 2;    grid-column: 2;    align-self: start;    @container card (inline-size < 15rem) {      grid-row: auto;      grid-column: 1;      justify-self: start;      margin-top: .375rem;    }  }  .card-body {    min-width: 0;  }  /* Actions at the foot of the card. Under 15rem of content they stack at full width, the last one first. */  .card-footer {    display: flex;    flex-wrap: wrap;    align-items: center;    justify-content: flex-end;    gap: .5rem;    margin-top: auto;    @container card (inline-size < 15rem) {      flex-direction: column-reverse;      align-items: stretch;    }  }  /*   * Split: from 28rem of content the media stands beside it, 4px inside the card on three sides.   * A container cannot restyle itself from its own query, so the media is placed against the card's edge and   * the other parts make room for it.   */  .card-split {    --card-split: .4;    @container card (inline-size >= 28rem) {      .card-media {        position: absolute;        inset-block: var(--card-media-inset);        inset-inline-start: var(--card-media-inset);        width: calc(100% * var(--card-split) - var(--card-media-inset));        height: calc(100% - var(--card-media-inset) * 2);        margin: 0;        aspect-ratio: auto;      }      .card-header, .card-body, .card-footer {        /* The media ends at --card-split of the card's width; a percentage here is of the content box. */        margin-inline-start: calc((100% + var(--card-padding) * 2) * var(--card-split));      }    }  }}