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:
- 2.4.4 Link Purpose (In Context) — the purpose of each link can be determined from the link text alone or together with its context
- 1.3.1 Info and Relationships — information and relationships conveyed through presentation must be programmatically determinable
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
::afterpseudo-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: relativeandz-index
Block Link Card
Block Link Card
<!-- Entire card wrapped in <a> — SR reads all content as one link name -->
<a href="/intro-react" 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><!-- Heading link with ::after pseudo-element extends click target -->
<article class="card">
<h3>
<a href="/intro-react" 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>Introduction to React
Learn the basics of React including components, state, and props for building modern user interfaces.
5 min readWhat’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
::afterpseudo-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
What the screen reader announces:
| Version | Announcement |
|---|---|
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
<!-- 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>Introduction to React
Learn the basics of React including components, state, and props.
<!-- Semantic heading, labeled buttons above the pseudo-element layer -->
<article class="card">
<h3>
<a href="/intro-react" 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>Introduction to React
Learn the basics of React including components, state, and props.
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
::afterpseudo-element covers the full card area, making the entire card clickable - Action buttons use
position: relativeandz-index: 1to sit above the pseudo-element layer, so they remain independently clickable - Each button has an
aria-labelproviding its purpose (“Save”, “Share”) - SVGs are marked
aria-hidden="true"since thearia-labelprovides the accessible name
What the screen reader announces:
| Version | Announcement |
|---|---|
| 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” |