Accessibility pattern · Disclosure and content

Card

The heading's link is the card's one link; a pseudo-element stretches it over the whole card, so the card is clickable without wrapping everything in an anchor. Save and Share sit above that layer with their own names, focus rings and Tab stops, and the heading comes first in the source even though the picture is drawn above it.

WCAG criteria
7
Keyboard rules
3
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Made by hand this month

  • Blue pottery vase

    Jaipur · Ceramics

    Quartz clay, painted by hand in cobalt and turquoise. 22 cm tall.

    ₹2,450

  • Dhokra brass elephant

    Bastar · Metalwork

    Cast by the lost-wax method, so no two are quite alike. 12 cm long.

    ₹3,200

  • Kantha cushion cover

    Bolpur · Textiles

    Layers of old cotton saris, quilted with running stitch. 40 × 40 cm.

    ₹1,150

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves to each card's link, then its Save and Share buttons, card by card.
EnterOn a card's link, follows it, like a click anywhere else on the card.
Enter or SpaceOn Save, saves or unsaves the item; on Share, shares the link or copies it and says so.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches a cardBlue pottery vase, link, heading level 4
Tab reaches SaveSave Blue pottery vase, toggle button, not pressed
Save is pressedPressed
Share copies the linkLink copied
A screen reader lists the linksBlue pottery vase, Dhokra brass elephant, Kantha cushion cover

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-card-grid" data-ap-card-grid>
  <h3 class="ap-card-grid__group">Made by hand this month</h3>
  <ul class="ap-card-grid__grid" role="list">
    <li class="ap-card-grid__item" id="card-vase">
      <h4 class="ap-card-grid__name"><a class="ap-card-grid__link" id="card-name-1" href="#card-vase">Blue pottery vase</a></h4>
      <p class="ap-card-grid__meta">Jaipur · Ceramics</p>
      <p class="ap-card-grid__desc">Quartz clay, painted by hand in cobalt and turquoise. 22 cm tall.</p>
      <div class="ap-card-grid__foot">
        <p class="ap-card-grid__price">₹2,450</p>
        <div class="ap-card-grid__actions">
          <button type="button" class="ap-card-grid__act" data-card-save aria-pressed="false" aria-labelledby="card-save-1 card-name-1">
            <svg class="ap-card-grid__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z"/></svg>
            <span id="card-save-1">Save</span>
          </button>
          <button type="button" class="ap-card-grid__act" data-card-share aria-labelledby="card-share-1 card-name-1">
            <svg class="ap-card-grid__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4v11M7.5 8.5 12 4l4.5 4.5M5 13v5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-5"/></svg>
            <span id="card-share-1">Share</span>
          </button>
        </div>
      </div>
      <p class="ap-card-grid__note" role="status"></p>
      <div class="ap-card-grid__media ap-card-grid__media--vase" aria-hidden="true">
        <svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid meet" focusable="false">
          <ellipse class="ap-card-grid__ink ap-card-grid__ink--soft" cx="160" cy="176" rx="64" ry="8"/>
          <path class="ap-card-grid__ink" d="M140 44h40v14c0 10 30 22 30 64 0 30-20 54-50 54s-50-24-50-54c0-42 30-54 30-64Z"/>
          <path class="ap-card-grid__line" d="M118 112c14 10 28 10 42 0s28-10 42 0M114 132c15 10 31 10 46 0s31-10 46 0"/>
          <path class="ap-card-grid__line ap-card-grid__line--thin" d="M136 44h48"/>
        </svg>
      </div>
    </li>
    <li class="ap-card-grid__item" id="card-elephant">
      <h4 class="ap-card-grid__name"><a class="ap-card-grid__link" id="card-name-2" href="#card-elephant">Dhokra brass elephant</a></h4>
      <p class="ap-card-grid__meta">Bastar · Metalwork</p>
      <p class="ap-card-grid__desc">Cast by the lost-wax method, so no two are quite alike. 12 cm long.</p>
      <div class="ap-card-grid__foot">
        <p class="ap-card-grid__price">₹3,200</p>
        <div class="ap-card-grid__actions">
          <button type="button" class="ap-card-grid__act" data-card-save aria-pressed="false" aria-labelledby="card-save-2 card-name-2">
            <svg class="ap-card-grid__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z"/></svg>
            <span id="card-save-2">Save</span>
          </button>
          <button type="button" class="ap-card-grid__act" data-card-share aria-labelledby="card-share-2 card-name-2">
            <svg class="ap-card-grid__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4v11M7.5 8.5 12 4l4.5 4.5M5 13v5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-5"/></svg>
            <span id="card-share-2">Share</span>
          </button>
        </div>
      </div>
      <p class="ap-card-grid__note" role="status"></p>
      <div class="ap-card-grid__media ap-card-grid__media--elephant" aria-hidden="true">
        <svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid meet" focusable="false">
          <ellipse class="ap-card-grid__ink ap-card-grid__ink--soft" cx="160" cy="176" rx="80" ry="7"/>
          <path class="ap-card-grid__trunk ap-card-grid__trunk--tail" d="M94 104c-10 6-12 16-8 26"/>
          <path class="ap-card-grid__ink" d="M100 128h20v44h-20ZM126 134h18v38h-18ZM164 134h18v38h-18ZM188 128h20v44h-20Z"/>
          <ellipse class="ap-card-grid__ink" cx="150" cy="112" rx="60" ry="40"/>
          <circle class="ap-card-grid__ink" cx="214" cy="98" r="30"/>
          <path class="ap-card-grid__trunk" d="M236 110c10 14 12 32 4 46-3 6-9 8-14 4"/>
          <path class="ap-card-grid__line ap-card-grid__line--thin" d="M204 78c-14 6-16 28-2 38M114 100c16-8 46-8 66 0M110 120c20-8 54-8 76 0"/>
          <path class="ap-card-grid__line ap-card-grid__line--thin" d="M228 122c4 6 10 8 16 5"/>
          <circle class="ap-card-grid__dot" cx="224" cy="92" r="3.5"/>
        </svg>
      </div>
    </li>
    <li class="ap-card-grid__item" id="card-cushion">
      <h4 class="ap-card-grid__name"><a class="ap-card-grid__link" id="card-name-3" href="#card-cushion">Kantha cushion cover</a></h4>
      <p class="ap-card-grid__meta">Bolpur · Textiles</p>
      <p class="ap-card-grid__desc">Layers of old cotton saris, quilted with running stitch. 40 × 40 cm.</p>
      <div class="ap-card-grid__foot">
        <p class="ap-card-grid__price">₹1,150</p>
        <div class="ap-card-grid__actions">
          <button type="button" class="ap-card-grid__act" data-card-save aria-pressed="false" aria-labelledby="card-save-3 card-name-3">
            <svg class="ap-card-grid__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 20s-7-4.4-7-10a4 4 0 0 1 7-2.6A4 4 0 0 1 19 10c0 5.6-7 10-7 10Z"/></svg>
            <span id="card-save-3">Save</span>
          </button>
          <button type="button" class="ap-card-grid__act" data-card-share aria-labelledby="card-share-3 card-name-3">
            <svg class="ap-card-grid__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4v11M7.5 8.5 12 4l4.5 4.5M5 13v5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-5"/></svg>
            <span id="card-share-3">Share</span>
          </button>
        </div>
      </div>
      <p class="ap-card-grid__note" role="status"></p>
      <div class="ap-card-grid__media ap-card-grid__media--cushion" aria-hidden="true">
        <svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid meet" focusable="false">
          <ellipse class="ap-card-grid__ink ap-card-grid__ink--soft" cx="160" cy="178" rx="70" ry="7"/>
          <path class="ap-card-grid__ink" d="M96 52c22 6 106 6 128 0 8 30 8 90 0 120-22-6-106-6-128 0-8-30-8-90 0-120Z"/>
          <path class="ap-card-grid__line ap-card-grid__line--dash" d="M110 74c20 4 80 4 100 0M110 96c20 4 80 4 100 0M110 118c20 4 80 4 100 0M110 140c20 4 80 4 100 0"/>
        </svg>
      </div>
    </li>
  </ul>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The cards are items of a list, each with a heading, so a screen reader says how many there are and can jump from name to name.

  • 1.3.2 Meaningful Sequence Level A

    The heading comes first in the source and the picture last, so a card reads name, place, description, price, though it is drawn picture first.

  • 2.4.4 Link Purpose (In Context) Level A

    The link text is the product's name, so a list of links reads as a list of products, not three Read more links.

  • 2.4.7 Focus Visible Level AA

    The link's focus ring is drawn around the whole card, and Save and Share have rings of their own.

  • 2.5.3 Label in Name Level A

    Save and Share are named by their visible word followed by the product name, so the name starts with what is on screen.

  • 4.1.2 Name, Role, Value Level A

    Save is a toggle button with aria-pressed, so whether the item is saved is announced.

  • 4.1.3 Status Messages Level AA

    Copying a link is confirmed in a status message inside the card, without moving focus.

Usage

When to use it

Use it

  • A grid of items of the same kind that each lead to their own page: products, articles, places.
  • Items with one main destination and a couple of quick actions.

Use something else

  • Several equally important links in one card: list them as separate links, without the stretched layer.
  • Long text people may want to select and copy: the stretched link catches every click on it.
  • Data people compare across items: a table lines the values up.

Common failures

How it usually goes wrong

  • The whole card wrapped in a link

    A link around everything reads the price, description and buttons as one long link name, and puts buttons inside a link. One stretched link keeps the name short.

  • Read more, three times

    Links that all say Read more are useless in a list of links. Here the product name is the link.

  • Buttons under the stretched layer

    If the link's pseudo-element covers Save, a click on Save opens the card instead. The actions sit above it, with a higher z-index.

  • The picture first in the source

    With the image first, a screen reader meets a picture before it knows what the card is about. The heading comes first; CSS draws the picture on top.

  • Actions that appear only on hover

    Save and Share revealed on hover cannot be found by touch, and appear from nowhere for keyboard users. They are always visible here.

  • Identical button names

    Three buttons called Save make a list of buttons useless. Each name here adds the product it belongs to.

Notes

Building it

  • The stretched link is an ::after on the link, positioned against the card; the card needs position: relative, and nothing in between may set its own position.
  • list-style: none removes list semantics in Safari; role="list" on the ul puts them back.
  • The pictures are decorative because the heading already names the product. If a picture adds information, give it alt text and keep it after the heading in the source.
  • Share uses the system share sheet on touch devices that have one and copies the link elsewhere, reporting the result in the card's status line.

Sources: Inclusive Components: Cards · WAI-ARIA Authoring Practices: Button (toggle)

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction