A table-like layout using buttons as cards
The question arose; Should I use clickable table rows or individual cards? The conclusion I came to was to use cards.
Example
Click a card button to see what would actually be read aloud by a screen-reader.
The code
HTML
<div class="cards_wrap">
<!-- Visual only, not read aloud by screen-readers -->
<!-- Do NOT hide if interactive - EG Sorting by column -->
<div class="card_column_headers" aria-hidden="true">
<div class="name">Payee name</div>
<div class="mandate">Mandate</div>
<div class="date">Date last payed</div>
<div class="amount">Amount</div>
</div>
<ul class="card_list" role="list">
<li>
<button class="card" type="button" aria-labelledby="labelledby2" aria-describedby="describedby2">
<!-- Totally hidden, only referenced by screen-readers. -->
<!-- Audio order may be better in a different order to the visual order -->
<!-- NOTE: Use punctuation! -->
<div hidden id="labelledby2">
Name: Mick Jagger,
Amount: £7,890.12,
Date: 10th January 2025
</div>
<!-- NOTE: Use punctuation! -->
<div hidden id="describedby2">
Mandate: 021,
Role: Vocalist,
Sort code: 12-34-56,
Account number: 1 2 3 4 5 6 7 8
</div>
<!-- Visual only, not read aloud by screen-readers -->
<div aria-hidden="true">
<div>
<div class="name">Mick Jagger</div>
<div class="account">12-34-56 | 12345678</div>
<div class="role">Vocalist</div>
</div>
<div class="mandate">021</div>
<div class="date">10/01/25</div>
<div class="amount">£7,890.12</div>
</div>
</button>
</li>
<!-- ... More li elements with buttons ... -->
</ul>
</div>
CSS
/* The button cards */
.cards_wrap {
/* Ensure 4.5:1 colour contrast ratio between foreground and background colours. Border colour needs to be 3:1 */
--_header-color: #1d7a9f;
--_header-bg-color: #f5f9fb;
--_card-color: #444;
--_card-secondary-color: #727272;
--_card-bg-color: #fdfdfd;
--_card-bg-color-even: #f5f9fb;
--_card-border-color: #CEDEE7;
--_card-focus-outline-color: blue;
line-height: 1.3;
}
/* Visual-only layout within the button card */
.cards_wrap div[aria-hidden=true] {
display: grid;
grid-gap: 1rem;
/* A simple grid here, a real world pattern is beyond scope */
grid-template-columns: 1fr 1fr 1fr 1fr;
}
.card_column_headers {
color: var(--_header-color, #333);
background-color: var(--_header-bg-color, #f7f7f7);
font-size: smaller;
font-weight: 700;
}
.card_column_headers,
.card {
padding: 8px 12px;
}
/* role="list" enforces list semantics for accessibility */
.card_list[role="list"] {
list-style: none;
padding:0;
}
.card_list li {
margin-top: 1rem;
}
.card {
outline: initial;
font: inherit;
color: var(--_card-color, #333);
background-color: var(--_card-bg-color, #f7f7f7);
border: 1px solid var(--_card-border-color, #959595);
width: 100%;
text-align: left;
cursor: pointer;
}
li:nth-child(even) > .card {
background-color: var(--_card-bg-color-even, red);
}
.card * {
pointer-events: none;
}
/* Unique settings for specific fields */
.cards_wrap .amount {
/* Always right align numbers with decimal places */
text-align: right;
}
.card .name {
font-weight: 500;
}
.card :is(.account, .role) {
font-size: .8rem;
font-weight: normal;
color: var(--_card-secondary-color, #333);
}
/* Keyboard-only focus rings */
a, button, summary, input, select, textarea, [contenteditable], [tabindex], .card {
outline: 2px solid transparent;
outline-offset: 8px;
}
:is(a, button, summary, input, select, textarea, [contenteditable], [tabindex], .card):focus-visible {
outline-color: var(--_input-focus-outline-color, blue);
outline-offset: 2px;
transition: all .3s ease-out;
transition-property: outline-color, outline-offset;
}
Features
- The header section is a visual only, hidden by aria-hidden, and not read aloud by a screen-reader. Don't do this if the headers are clickable for sorting (beyond scope).
- Cards are presented as a list of buttons.
- The card container is a button element for the best semantics, built in accessibility, and the least JavaScript.
- Button content is vocalised primarily by
aria-labelledby
indexed non-visual content, which allows the audio order to slightly differ from the visual order. - Button secondary content vocalisation is supplied by
aria-describedby
indexed non-visual content, which allows the audio order to slightly differ from the visual order. - Visual and audio order are completely separated. For example: Name and Amount may be most important fields but they're the furthest apart visually.
Caveat: Reflow at mobile viewports, 200% font-size, and dark mode, are beyond the scope of this demo.