Skip to Content
Component ExamplesCards

Cards

Content cards with headings, descriptions, and actions — the block link pattern done right.

Maintained by Martin KrugerLast reviewed

Overview

Cards are ubiquitous on the modern web — blog posts, product listings, dashboards. The most common accessibility mistake is wrapping the entire card in a single <a> tag. This forces screen readers to read every piece of content inside the card as one giant link name, making it nearly impossible to parse. The correct approach uses a heading link with a CSS ::after pseudo-element to extend the clickable area to the full card, giving users a concise link name while preserving full-card clickability for mouse users.

WCAG Criteria:

Key requirements:

  • Never wrap an entire card in a single <a> — screen readers concatenate all content as the link name
  • Use a heading link with a ::after pseudo-element to extend the click target to the full card
  • Use <article> for semantic card content
  • Each card must have a heading element
  • When cards have multiple actions, position action buttons above the pseudo-element layer with position: relative and z-index

View inaccessible code
<!-- Entire card wrapped in <a> — SR reads all content as one link name --> <a href="https://react.dev/learn" class="card"> <h3>Introduction to React</h3> <p>Learn the basics of React including components, state, and props for building modern user interfaces.</p> <span>5 min read</span> </a>

Introduction to React

Learn the basics of React including components, state, and props for building modern user interfaces.

5 min read
View accessible code
<!-- Heading link with ::after pseudo-element extends click target --> <article class="card"> <h3> <a href="https://react.dev/learn" class="card-link">Introduction to React</a> </h3> <p>Learn the basics of React including components, state, and props for building modern user interfaces.</p> <span>5 min read</span> </article> <style> .card { position: relative; } .card-link::after { content: ''; position: absolute; inset: 0; } </style>

What’s wrong with wrapping the card in <a>?

  • Screen readers announce the entire card content as a single link: “Introduction to React Learn the basics of React including components, state, and props for building modern user interfaces. 5 min read, link” — a wall of text
  • The heading, description, and meta are no longer independent elements — they all collapse into one link name
  • Users cannot select text inside the card
  • The heading loses its semantic role within the link context in some screen readers

How the ::after technique works:

  • The link sits inside the <h3>, giving it a concise accessible name: “Introduction to React”
  • The ::after pseudo-element on the link is positioned absolutely over the entire card (inset: 0)
  • Mouse users can click anywhere on the card and follow the link
  • Screen reader users hear a properly scoped link name

Predicted semantic summary (exact output varies):

VersionAnnouncement
Inaccessible (whole card as <a>)“Introduction to React Learn the basics of React including components, state, and props for building modern user interfaces. 5 min read, link”
Accessible (heading link + ::after)“Introduction to React, link” (heading) then “Learn the basics of React…” (paragraph) and “5 min read” (text) as separate elements

Card with Multiple Actions

Card with Multiple Actions

Inaccessible

Introduction to React

Learn the basics of React including components, state, and props.

View inaccessible code
<!-- No heading, no button labels — SR users get no context --> <div class="card"> <p><b>Introduction to React</b></p> <p>Learn the basics of React including components, state, and props.</p> <div class="actions"> <button><svg><!-- save icon --></svg></button> <button><svg><!-- share icon --></svg></button> </div> </div>
Accessible

Introduction to React

Learn the basics of React including components, state, and props.

View accessible code
<!-- Semantic heading, labeled buttons above the pseudo-element layer --> <article class="card"> <h3> <a href="https://react.dev/learn" class="card-link">Introduction to React</a> </h3> <p>Learn the basics of React including components, state, and props.</p> <div class="actions"> <button aria-label="Save" class="action-btn"> <svg aria-hidden="true"><!-- save icon --></svg> </button> <button aria-label="Share" class="action-btn"> <svg aria-hidden="true"><!-- share icon --></svg> </button> </div> </article> <style> .card { position: relative; } .card-link::after { content: ''; position: absolute; inset: 0; } .action-btn { position: relative; z-index: 1; /* sits above the ::after layer */ } </style>

What’s wrong with the inaccessible version?

  • No heading — screen reader users have no way to quickly identify or navigate to the card
  • Bold text (<b>) is visual only; it does not convey structure to assistive technology
  • Icon buttons have no accessible names — screen readers announce just “button” with no indication of what the button does
  • SVGs inside buttons are not hidden with aria-hidden, so some screen readers may attempt to announce SVG markup
  • The card uses a <div> instead of <article>, providing no semantic meaning

How the multiple-actions pattern works:

  • The heading link’s ::after pseudo-element covers the full card area, making the entire card clickable
  • Action buttons use position: relative and z-index: 1 to sit above the pseudo-element layer, so they remain independently clickable
  • Each button has an aria-label providing its purpose (“Save”, “Share”)
  • SVGs are marked aria-hidden="true" since the aria-label provides the accessible name

Predicted semantic summary (exact output varies):

VersionAnnouncement
Inaccessible (no heading, no labels)“Introduction to React” (bold text, no structure), “button” (no name), “button” (no name)
Accessible (heading + labeled actions)“heading level 3, Introduction to React, link”, “Save, button”, “Share, button”

Resources