Lately, I have been working a lot on standardizing for myself and for my clients how Interactive Cards should be built. To be honest, it took me a lot of time researching, building, testing and iterating to find the right examples to make this UI pattern accessible.
First of all, we have to understand what a Card is. A Card is a UI pattern really helpful to group information in an engaging way. That information can be an image, a link, a paragraph, a button, heading, etc. Interactive Cards are an entry point into more information because the user can click a button to go to the PDP (Product Description Page) of a product or save a product to favorites, etc.
What we need to know before building an interactive card:
- Know the difference between links and buttons. (pppss, you can learn the difference here)
- Know how to make the card interactive without breaking the logical HTML semantic.
Interactive cards can be organized in two different categories: Simple and Complex. But first, we have to learn what a card is.
Static Cards
Since a Card is a visual group of a specific semantic concept, we have to use an HTML element that conveys that meaning and that element is... an ARTICLE. According to HTML Standard, an article is:
The article element represents a complete, or self-contained, composition in a document, page, application, or site and that is, in principle, independently distributable or reusable, e.g. in syndication. This could be a forum post, a magazine or newspaper article, a blog entry, a user-submitted comment, an interactive widget or gadget, or any other independent item of content.
So, cool, an Static Card should be built using an <article>. We can move forward. Well, yes and no because I am pretty sure that only 10% of developers know how to build an accessible card. Let's break the Static Card example to understand its anatomy.
Anatomy
-
<article>wraps the whole card. -
aria-labelledby="title"its value should be an IDREF pointing to theidvalue on the main heading inside the article. -
<h3 id="title">is the main topic of the card. -
<p>includes a description or extra information related to the topic that the article groups.
The whole anatomy structure of a Static Card can be found on this link
Code Example
<article aria-labelledby="id-of-the-heading">
<img src="/link-to-the-source" alt="">
<h2 id="id-of-the-heading">Main title of the article</h2>
<p>Extra information or a description of the topic that the article groups</p>
</article>
Interactive Cards
Dynamic Cards are a whole different world. They combine the article structure and the interactive behavior of a link or a button and this is an extremely dangerous zone where all of you fail because you do not even know how to do a simple static card or to differentiate a link from a button. There could be a card that redirects the user to another location, opens a modal, when it receives focus or hover changes the images, expands the content or has complex interactions inside of it. I am going to cover all those cases and explain their risks and how to mitigate them.
Anatomy
First Layer: <article>
Should be a plain article. You should not force the interaction by adding a tabindex="0" or a tabindex="-1" to force the focus programmatically or a onclick to force the functionality in JavaScript. The accessible name should be passed by adding an aria-labelledby pointing to the IDREF in the main heading of the card. The final step that will do the magic later is adding in CSS a position: relative. This property will help us to stretch that makes it the containing block the stretched pseudo-focus on the whole card when the main interactive element receives focus.
<article aria-labelledby="card-title">
{content}
</article>
article {
position: relative;
}
Second Layer: <a>
The interactive element that will be stretched to make the whole card interactive with the techniques in points 3 and 4.
<h3 id="card-title">
<a href="/kyoto" className="card__link">Kyoto, day to night</a>
</h3>
Third Layer: ::after to stretch the action target
An empty pseudo-element on the link, absolutely positioned with inset: 0, so the entire card surface becomes the link's hit area for mouse and touch, and .card:hover fires anywhere on it and the whole article should have a position: relative. Screen reader users interact with the real link in layer 2. pss, you can make the text selectable by adding user-select: text; to force it.
.card__link::after {
content: "";
position: absolute;
inset: 0;
}
article {
position: relative;
}
article > p {
user-select: text;
}
Fourth Layer: article:has([element]:focus-visible)
When the stretched interactive element in the interactive card receives focus a simil focus outline should wrap the whole card to point out that the whole area is interactive.
article:has(a:focus-visible) {
outline: 2px solid black;
outline-offset: 4px;
}
Here you can interact with the interactive anatomy. It worths the time.
Simple Interactive Cards
- Only have one interactive element with a single purpose which could be redirecting the user to a new location or expanding more information.
- One stretched
<a>or<button>with a::afterpseudo-element withposition: absolute; inset: 0, if applicable. - The card itself,
article, should never be the interactive element. - One Tab stop per card.
- When the link is focused, visually highlights the entire card.
- The screen reader announces the elements in a hierarchical way: first the article with its accessible name, then the heading, description, action, for example.
- Can be used on: blog or news teasers, category tiles, team member cards, promo cards with a single CTA.
<article class="card" aria-labelledby="hotel-1">
<h3 id="hotel-1">
<a class="card__link" href="#">Hotel</a>
</h3>
<p>Information about the hotel</p>
</article>
Complex Interactive Card
- Have two or more interactive elements, each with its own purpose. For example: a card with an 'add to cart' button, a 'save to favorites' button and a 'go to product details' link.
- The interactive elements are siblings, never nested because this is an accessibility failure and a bad practice.
- The card can have a stretched interactive element but only if it has a clear destination. For example, a link to go to the PDP page.
- One tab stop per control: primary link first then, secondary actions.
- If applicable, when focusing on the primary link, it highlights the entire card using a streched link technique.
- As in Simple Interactive Card, the screen readers should announce first the article with its accessible name, then the heading with the primary link, then the description and then the rest of the actions.
- Can be used on: e-commerce product cards (Add to cart, save to favorites), social media posts (Like, Reply, share), articles with a Bookmark button.
<article class="card" aria-labelledby="product-1">
<h3 id="product-1">
<a class="card__link" href="#">Product 1</a>
</h3>
<p>Description of the product 1</p>
<button class="card__control">Add product 1 to cart</button>
<button class="card__control" aria-pressed="false">
<span class="visually-hidden">Save Product 1 to favorites</span>
</button>
</article>
Use Cases
Well, the use cases can be seen in this page because it would be take me so much time to migrate 14 uses cases in codesandbox in dev.to.
WCAG 2.2 success criteria met
| SC | Level | Met by |
|---|---|---|
| 1.3.1 Info and Relationships | A | Every card is an <article> named by its heading through aria-labelledby
|
| 2.1.1 Keyboard | A | Every action is a native <a> or <button> and receives focus with the keyboard |
| 2.4.3 Focus Order | A |
Tab order follows the DOM and the stretched link adds no extra tab stop, then, if applicable, the focus is placed on the next interactive element inside the card |
| 2.4.4 Link Purpose (In Context) | A | Link text such as "Kyoto, Japan" and "Learn more about Kyoto" is clear next to the card's heading |
| 2.4.7 Focus Visible | AA | The whole card is outlined through :has(:focus-visible) when its stretched link has focus. Secondary controls have their own focus ring |
| 4.1.2 Name, Role, Value | A | Native a/button have role and name; native <details>/<summary> exposes expanded/collapsed state without ARIA; aria-pressed on the save toggle. |
Top comments (0)