Skip to main content

Core · Show content

Card

Groups a few facts about one thing on a bordered surface.

Example

Two cards, each with a title and one fact about a service.

API gateway

1,204 requests per minute.

Database

48 active connections.

Known issues

In a linked card, the body text, the price included, cannot be selected with a mouse: the title link covers the whole card, so a drag follows or moves the link. Find in page, keyboard selection and screen readers reach the text as usual.

Code

Copy the markup

HTML
<article class="luz-card">
  <h3 class="luz-card__title">Database</h3>
  <p class="luz-card__body">48 active connections.</p>
</article>

Variants and options

One card, six classes and two modifiers. A link in the title makes the whole card one link; any other link or button in it is raised above that link.

Class or markupWhat it does
.luz-card
The surface: border, rounded corners and padding
.luz-card__title
Base size, semibold, primary text colour
.luz-card__body
Medium size, secondary text colour
.luz-card__price
The price: base size, semibold, primary text colour, figures of equal width. A previous price goes in <s>
.luz-card--featured
The one card to look at first in a group: a 2 px border in the brand colour, as tall as its neighbours. Say why in words too, with a tag such as “Most chosen”
.luz-card--media
An image on top, a body and actions at the foot: a flex column with no padding of its own, so the image runs to the border
.luz-card__media
The <img>, full width, cropped to --luz-card-ratio (16 / 9 unless you set it), rounded on top only
.luz-card__actions
The links or buttons, pushed to the foot, so those of cards side by side line up
<a> in .luz-card__title
A linked card: the link covers the card, the title turns sans, a darker border and a shadow on hover, a focus ring round the card

Compose a card with the layout objects and the other components, with no new class: a linked card, an avatar or an icon beside the text with .o-media, an image on top, and a row of actions with .o-row.

Table

Rows of records that share the same attributes.

Carlos Díaz

Looks after billing and invoices.

Soft dunes in warm light under a clear blue sky

Dunes at sunset

A two-hour walk from the car park.

Database

48 active connections.

Running
  • Linked card: the <a> goes inside the heading, round its text. A second link or a button in it is allowed, and sits above the title link: see Product card below.
  • Avatar or icon: .o-media puts it beside .o-media__body, which holds the title and the body. Next to a name, hide the avatar with aria-hidden="true": it repeats the name.
  • Image on top: the image frame (.luz-image), inside the padding, with .u-mb-sm under it. Its alt text says what the picture shows; if it only repeats the title, leave it empty (alt="").
  • Actions: .o-row with small buttons (.luz-btn--sm) or tags (.luz-tag), last in the card. In a linked card, one action at most, as a button after the title.

Product card

A card for a shop is the card with an image, a linked title, a price and one action. The action is a button in a form, so it works without JavaScript; it is raised above the title link, so it takes its own click.

Red wines
  • Malbec 2021

    Bodega Albar · 75 cl

    12,50 €

  • Tempranillo reserva 2019

    Bodega Albar · 75 cl

    Now 9,90 € Was 12,50 €

HTML
<article>
  <h4 id="red-wines">Red wines</h4>
  <ul class="o-grid o-grid--auto" aria-labelledby="red-wines">
    <li class="luz-card">
      <div class="luz-image luz-image--4x3 u-mb-sm">
        <picture>…<img src="malbec.jpg" alt="" width="640" height="480" loading="lazy" decoding="async"></picture>
      </div>
      <h5 class="luz-card__title"><a href="/wines/malbec-2021/">Malbec 2021</a></h5>
      <p class="luz-card__body">Bodega Albar · 75 cl</p>
      <p class="luz-card__price"><data value="12.50">12,50 €</data></p>
      <form action="/cart/add" method="post">
        <input type="hidden" name="product" value="malbec-2021">
        <button class="luz-btn luz-btn--primary luz-btn--block" type="submit">Add to cart<span class="u-sr-only"> Malbec 2021</span></button>
      </form>
    </li>
  </ul>
  <output class="u-sr-only" id="cart-status"></output>
</article>
  • The button is the width of the card with .luz-btn--block: a thumb finds it, and its label wraps and breaks inside the card at any text size.
  • One action, one link. The name is the link; the button is the action. A card with a second action (a wish list, a comparison) has outgrown a card: send the reader to the product's own page.
  • The button's name is unique. The visible words come first, then the product's name in .u-sr-only, repeating the link's text exactly (keep the space before it). A list of twelve buttons all called “Add to cart” says nothing to someone who lists the buttons of a page, or says them by voice.
  • The list is named by its heading: aria-labelledby points at the <h4> above it, and the cards use the next level. Core styles no list: remove the bullets and the indent with your own rule (list-style: none; margin: 0; padding: 0).
  • The old price is <s>, with the words “Now” and “Was” hidden visually so the order is clear to a screen reader; the line through it is not colour.
  • Feedback after a script adds the item: keep an empty <output> (it has the role status) in the page from the start and write “Malbec 2021 added to the cart” into it; focus stays on the button. A status region that is created together with its text is often not announced. Without a script the form posts and the cart page is the feedback.
  • Sold out: replace the form with <span class="luz-tag">Sold out</span>; do not disable the button.
  • With core alone there is no image frame: use an <img> with your own size rules.

In a group of plans or offers, .luz-card--featured marks the one to look at first. The class says nothing to a screen reader, so the card carries a tag with the reason, in its text.

One day

79 €

The talks and the workshops of one day.

Choose One day
Team

590 €

Two days for five people, with a shared table.

Choose Team
HTML
<article class="luz-card luz-card--featured">
  <h4 class="luz-card__title">Two days</h4>
  <p class="luz-card__price"><data value="149">149 €</data></p>
  <p class="luz-card__body">Both days, lunch and the recordings.
    <span class="luz-tag luz-tag--blue">Most chosen</span></p>
  <a class="luz-btn luz-btn--primary luz-btn--block" href="/register/?ticket=two-days">Choose Two days</a>
</article>
  • One featured card per group. If everything is featured, nothing is.
  • The words are required. A 2 px border is a second cue for people who see it, and in forced colors the only one; a tag such as “Most chosen” or “Best value” is the one that reaches everybody. Put it in the body or beside the title.
  • It stays as tall as its neighbours: the extra pixel of border is taken from the padding.

Media card

For a project, a post or a place: an image, a few words and one or more actions. .luz-card--media makes the card a column, the image runs to its edge, and the actions sit at the foot, so in a grid the buttons line up whatever the length of the text.

Site
Dunes

A two-hour walk.

HTML
<article class="luz-card luz-card--media">
  <img class="luz-card__media" src="tides.webp" alt="" width="1200" height="675">
  <div class="luz-card__body o-stack">
    <h3 class="luz-card__title">Tide table</h3>
    <p>Tides for any harbour on the coast.</p>
  </div>
  <div class="luz-card__actions o-actions">
    <a class="luz-btn luz-btn--primary" href="/tides/demo/">Open the demo<span class="u-sr-only"> of Tide table</span></a>
    <a class="luz-btn luz-btn--secondary" href="/tides/code/">View the code<span class="u-sr-only"> of Tide table</span></a>
  </div>
</article>
  • One destination: link the title. The whole card, image included, is the link, and a card with no __actions needs none. Several destinations: no link in the title. Put each in __actions: a link round the title would cover the card and compete with the buttons.
  • Name each action once on the page. The visible words first, then the card's title in .u-sr-only, so a list of links says which project each one opens.
  • The image is the one thing that may be decoration. When the title says it all, leave alt=""; otherwise say what the picture shows. Always give width and height, so the card does not jump when the image loads.
  • The crop is a property. --luz-card-ratio: 4 / 3 on the card, or on a parent, changes the shape for all its cards; the image is cropped, never stretched.
  • Two actions at most, side by side when they fit and stacked at 640 px and below (.o-actions).

How it works

A container styled by components/card.css. No JavaScript.

  • Use any element: <article> for a self-contained item, <div> for a group of facts, <li> inside a list of cards.
  • The title class goes on the heading level that fits the page outline.
  • The corners are rounded with --luz-radius-md, like a figure or a dropdown.
  • The last title or body has no space under it: the padding is the card's last space.
  • .luz-card--media is display: flex in a column with padding: 0: the image has none, the body and the actions take --luz-space-md. The actions have margin-block-start: auto, which pushes them to the foot of a card stretched to the height of its row. The image keeps aspect-ratio: var(--luz-card-ratio, 16 / 9) with object-fit: cover, and its top corners are the card's radius less the 1 px border; the card clips nothing, so the focus ring is whole.
  • A linked card: the link's ::after is stretched over the card (position: absolute; inset: 0), so a click anywhere in it follows the link. The card is position: relative to hold it. Any other link or control inside the card (a, button, input, select, textarea, summary) is position: relative with z-index: 1, so it paints above that overlay and takes its own click; the title link is the one thing never raised. A button's label wraps in a card (white-space: normal), so a long label or enlarged text stays inside it. On hover the border turns --luz-input-border and a small shadow shows; on keyboard focus a ring goes round the card. The title of a linked card is in sans, as a nav or a list of links is; a card without a link keeps the heading face. Both are found with :has(); a card without a link does not change on hover.
  • .luz-card--featured sets border-width: 2px in --luz-brand-primary and takes the extra pixel from the padding, so the card keeps the height of its neighbours. In a linked card the border turns --luz-brand-hover on hover, and the keyboard ring stands 4 px off the border, so it does not read as a thicker border.
  • To lay cards out, use the grid objects, such as the split grid for two cards side by side.

Without JavaScript

Nothing changes: a card, linked or not, a media card included, is HTML and CSS.

Keyboard and ARIA

A card is not interactive and takes no focus.

  • In a card that is not linked, links and buttons inside keep their own behaviour.
  • A link or a button in a linked card is raised above the title link, so the pointer reaches it. It has its own tab stop, in source order, and its own ring; give each a name that is unique on the page (see Product card).
  • A linked card has a tab stop for its title link and one for each other link or button. Enter follows the title link; Enter or Space presses a button. The ring shows round the title link and round the whole card; a button shows its own ring only.
  • A media card with actions has one tab stop per action, in source order, and no stop or ring of its own: the title is not a link. With one destination it is a linked card, as above.
  • A screen reader names the link with the title, then reads the body as text. Add no role, tabindex or aria-label to the card.
  • Put the link on the title text, never round the card: a link round the card takes every word in it as its name.

Contrast and focus are in the Design tab.

Design

When to use

Use a card to group a small set of facts that read as one unit, often in a grid of similar items.

  • A featured card (.luz-card--featured) for the one recommended choice in a group of plans or offers.
  • A media card (.luz-card--media) for a project, a post or a place with a picture, one destination or two actions.

When not to use

Do not use a card to box content that is not a separate thing.

  • For rows of records with the same attributes, use a table.
  • For a message about the page, use an alert.
  • For a list of links, each with one line of description, use a plain list (<ul>): a card per link adds borders and nothing else.
  • A card with more than two actions, or a form in it, is a page: use the page of the thing, not a card.
  • Do not put a card inside a card.
  • Do not feature several cards in a group, or use the border as the only cue: say why in words too.

Do and don't

Do Put the link on the title: the whole card takes the click and the link is named by the title. An action goes after it as a button, with a name of its own.
Don't A link round the whole card, or a second link such as “Read more” in it: the first takes every word of the card as its name, the second sits under the title link and says nothing out of context.
Do With two destinations, leave the title without a link and put both in the actions, each named for its card.
Don't A linked title and actions in one card: the card takes a click for a third destination, and the title link repeats one of the buttons.
Do Feature one card, and say why in words: the border draws the eye, the tag tells everybody.
Don't A featured card with no words: a reader who does not see the border, or the colour, gets no hint that it is the one to choose.

Writing the content

A short noun as the title, then one or two lines.

  • No full stop on the title.
  • Keep cards in one group parallel: the same kind of title, the same length of body.
  • In a linked card, the title is the link text: it says where the link goes, so the card needs no “Read more”.
  • On a featured card, a tag of one to three words says why: “Most chosen”, “Best value”. A button names the plan: “Choose Two days”.
  • A media card: a title of one to four words, a body of two lines at most, and actions that start with a verb (“Open the demo”, “View the code”). The same words on every card of a group.
  • A price with its currency, in the format of the page's language. A button says what it does and for what: “Add to cart”, then the product's name for assistive technology.

Contrast and focus

The title is primary text and the body secondary text, on the card surface. In a linked card the title is a link, and the border on hover and the focus ring sit between the card and the page.

A featured card has a brand border against both.

PartColour on backgroundLightDarkHigh contrastNeeds
Title
--luz-text-primary on --luz-surface
16.5:1
14.0:1
21.0:1
4.5:1
Price
--luz-text-primary on --luz-surface
16.5:1
14.0:1
21.0:1
4.5:1
Body
--luz-text-secondary on --luz-surface
6.4:1
6.9:1
19.3:1
4.5:1
Title link
--luz-brand-primary on --luz-surface
5.9:1
7.2:1
14.1:1
4.5:1
Border of a linked card, on hover
--luz-input-border on --luz-surface
3.9:1
3.9:1
21.0:1
3:1
Border of a linked card, on hover, against the page
--luz-input-border on --luz-surface-page
3.7:1
4.2:1
21.0:1
3:1
Border of a featured card
--luz-brand-primary on --luz-surface
5.9:1
7.2:1
14.1:1
3:1
Border of a featured card, against the page
--luz-brand-primary on --luz-surface-page
5.6:1
7.7:1
14.1:1
3:1
Border of a linked featured card, on hover
--luz-brand-hover on --luz-surface-page
7.7:1
10.2:1
17.9:1
3:1
Focus ring round a linked card
--luz-focus-ring on --luz-surface-page
5.6:1
7.7:1
21.0:1
3:1

Measured from the token values of each mode, rounded down. Needs is the WCAG 2.2 AA minimum: 4.5:1 for text, 3:1 for focus rings, borders and icons.

  • Focus: a plain card takes none; the links and buttons inside keep their own ring. In a linked card the title link keeps its ring, and a second one, 2 px of --luz-focus-ring, goes round the card, 2 px outside its border (4 px on a featured card, whose own border is 2 px thick). Both are outlines, so they stay in forced-colors mode.
  • Edges: the border (--luz-divider) is decorative and has no minimum: the content and the space around it group the card. On a linked card the hover border is --luz-input-border, 3:1 or more, with a shadow.
  • Not colour alone: a featured card has a border twice as thick (2 px against 1 px), which stays in forced colors when the colour does not, and a tag that says why. The tag is the cue that does not depend on seeing.

Tokens it uses

The 19 tokens that components/card.css reads, by family. Each family links to its page, with the values in every mode. In Figma they are variables with the same names: see Tokens in Figma.

Spacing and sizing
--luz-space-2xs, --luz-space-md, --luz-space-sm
Radius
--luz-radius-md
Typography
--luz-font-sans, --luz-text-base, --luz-text-md
Shadows
--luz-shadow-sm
Motion
--luz-duration-base, --luz-duration-fast, --luz-easing-enter

Keyboard and ARIA are in the Code tab.