Responzivní tabulky pomocí ARIA rolí
Skutečná <table> je vždy první volba a postup, jak ji na úzké obrazovce přeskládat, popisuje záznam Responzivní přístupné tabulky. Někdy ale tabulkové prvky použít nejde, třeba když tabulku vygeneruje knihovna komponent nebo když se řádky skládají do mřížky. Pak je potřeba tabulkovou sémantiku dodat rolemi ARIA a s tím přicházejí pasti, hlavně při responzivním rozložení. Jde o údaje zapsané v kódu dvakrát a o rozdíl mezi skrytím jen vizuálním a skrytím pro čtečku.
1. Kdy má tabulka z prvků div smysl
První pravidlo ARIA říká, že kde existuje nativní prvek se správnou sémantikou, použije se ten. Tabulka z prvků <div> s rolemi je výjimka pro případy, kdy nativní tabulku opravdu použít nejde:
- Komponenta, kterou nemůžeme měnit, generuje prvky
<div>a tabulková sémantika se musí doplnit rolemi. - Řádky se při zmenšení skládají do mřížky nebo karet způsobem, který tabulkové hodnoty
displaynezvládají. - Tabulku vykresluje skript po částech, například virtualizovaný seznam s tisíci řádků, kde v kódu není vždy celá tabulka.
Ve všech ostatních případech je nativní <table> jednodušší, robustnější a nemusí se u ní nic doplňovat.
2. Tabulka z prvků div s rolemi
Role odpovídají prvkům, které nahrazují:
| Nativní prvek | Role | Poznámka |
|---|---|---|
<table> |
table |
Název přes aria-label nebo aria-labelledby, místo <caption>. |
<thead>, <tbody> |
rowgroup |
Seskupení řádků nadpisů a řádků s daty. |
<tr> |
row |
Jeden řádek. |
<th scope="col"> |
columnheader |
Nadpis sloupce. |
<th scope="row"> |
rowheader |
Údaj, který pojmenovává celý řádek, obvykle první sloupec. |
<td> |
cell |
Datová buňka. |
<div role="table" aria-label="Přehled dokumentů">
<div role="rowgroup">
<div role="row">
<div role="columnheader">Název dokumentu</div>
<div role="columnheader">Typ</div>
<div role="columnheader">Datum</div>
</div>
</div>
<div role="rowgroup">
<div role="row">
<div role="rowheader">Smlouva_o_dilo.pdf</div>
<div role="cell">Smlouva</div>
<div role="cell">30. 10. 2026</div>
</div>
</div>
</div>
Buňky mají být přímými potomky řádku. Obalový <div> bez role odečítače většinou přeskočí, ale přímý vztah je spolehlivější a snáz se kontroluje. Název souboru nebo jiný údaj, který záznam pojmenovává, patří do rowheader, protože odečítač při pohybu po buňkách pak ví, o který záznam jde.
Oproti nativní tabulce tu nevzniká past, kterou popisuje záznam Responzivní přístupné tabulky. Prvek <div> žádnou vlastní roli nemá, takže mu ji změna display nemůže odebrat, a tabulka zůstane tabulkou při jakémkoli rozložení.
Pokud tabulka zobrazuje jen část řádků nebo sloupců, například při stránkování, je potřeba odečítači navíc říct celkový počet pomocí aria-rowcount a aria-colcount. Ty vysvětluje článek Tabulka a grid prakticky: aria-colcount, aria-rowcount a další atributy.
3. Past: skrytí jen vizuálně nestačí
U responzivní tabulky se snadno stane, že se tentýž údaj zapíše do kódu dvakrát, jednou pro široké a jednou pro úzké rozložení, a nepoužívaná kopie se skryje třídou visually-hidden. Ta ale prvek skrývá jen vizuálně. Odečítač ho dál čte, a tabulka má proto o sloupec nebo řádek víc, než je vidět. Hodnoty se pak čtou pod nadpisy, které k nim nepatří, a poslední sloupec zůstane bez nadpisu. Stejně se chová druhý, jen vizuálně skrytý řádek s nadpisy sloupců.
Jednotlivé techniky skrytí se liší tím, komu prvek zmizí:
| Technika | Vidí ji oko | Vidí ji odečítač |
|---|---|---|
display:none nebo atribut hidden |
ne | ne |
visibility:hidden |
ne | ne |
třída visually-hidden, viz Skrytý text pro čtečky obrazovky (sr-only) |
ne | ano |
aria-hidden="true", viz Skrytí prvku před čtečkami obrazovky (aria-hidden) |
ano | ne |
Správné řešení je zapsat každý údaj do kódu jen jednou a na úzké obrazovce ho přesunout pomocí CSS, jak ukazuje další část. Kdyby duplicita přesto byla nutná, musí se nepoužívaná kopie skrýt přes display:none, protože jen tak zmizí i ze stromu přístupnosti.
Pozor na aria-hidden u buňky tabulky. Odečítač pak buňku neuvidí a řádek bude mít míň buněk než nadpisů, takže se ostatní hodnoty přiřadí ke špatným sloupcům. Atribut aria-hidden shrnuje článek Další parametry prakticky: aria-describedby, aria-hidden a aria-disabled.
Zkontrolovat to jde odečítačem. Po vstupu do tabulky NVDA oznámí počet sloupců a řádků. Pokud se liší od toho, co tabulka skutečně obsahuje, je v kódu duplicita.
4. Přeskládání tabulky s více sloupci
Každý řádek je grid a buňky jsou jeho přímými potomky. Při zužování se mění jen definice mřížky a umístění buněk přes grid-area. Pořadí v kódu i obsah zůstávají stejné a žádný údaj se neduplikuje. U pěti sloupců se vyplatí dva kroky místo jednoho:
- Na široké obrazovce je všech pět údajů vedle sebe v jednom řádku a nad nimi viditelný řádek s nadpisy sloupců.
- Na střední šířce je každý záznam karta. V prvním řádku je název a tlačítko, pod nimi další údaje ve třech sloupcích, každý s popiskem.
- Na úzké obrazovce je všechno pod sebou v jednom sloupci.
.docs__row {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(0, 1.6fr) minmax(0, 1.5fr) minmax(0, 1fr) 7rem;
grid-template-areas: "name type date size action";
}
.docs__name { grid-area: name; }
.docs__type { grid-area: type; }
.docs__date { grid-area: date; }
.docs__size { grid-area: size; }
.docs__action { grid-area: action; }
@media (max-width: 40em) {
.docs__row--head {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.docs__body .docs__row {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-areas:
"name name action"
"type date size";
}
.docs__body [data-label]::before {
display: block;
content: attr(data-label);
content: attr(data-label) / "";
}
}
@media (max-width: 28em) {
.docs__body .docs__row {
grid-template-columns: minmax(0, 1fr);
grid-template-areas: "name" "type" "date" "size" "action";
}
}
Řádek s nadpisy sloupců se na střední a úzké obrazovce nesmí skrýt přes display:none, protože by zmizel i odečítači. Schová se jen vizuálně, takže nadpisy zůstávají v DOM a vizuálně je nahrazují popisky z data-label. Druhý zápis content s lomítkem a prázdným řetězcem za ním nastavuje popisku prázdný alternativní text, aby ho odečítač nečetl podruhé po nadpisu sloupce.
Přesouvání buněk přes grid-area může rozejít vizuální pořadí s pořadím v kódu, v jakém buňky prochází odečítač a klávesnice. Souvislost vysvětluje záznam Pořadí prvků v kódu a vizuálně. V ukázce se přesouvá jen tlačítko, které je na kartě vizuálně v prvním řádku, ale v kódu až za údaji pod ním. U karty s víc ovladatelnými prvky by to mohlo způsobit nelogické pořadí fokusu, takže u nich je lepší pořadí v kódu a na obrazovce držet pohromadě.
5. Řazení sloupců
Řaditelný sloupec má v záhlaví tlačítko a atribut aria-sort je na prvku columnheader, ne na tlačítku. Přístupné řazení sloupců má takhle vypadat:
<div role="columnheader" aria-sort="descending">
<button type="button">Datum</button>
</div>
Hodnotu ascending nebo descending má jen sloupec, podle kterého se právě řadí, ostatním se atribut vynechá. Odečítač ale změnu aria-sort po stisku tlačítka sám nepřečte, protože se změnil rodič fokusovaného prvku, ne prvek samotný. Po kliknutí je proto potřeba do skryté oblasti s role="status" zapsat krátkou stavovou zprávu pro čtečky obrazovky, například „Seřazeno podle data, od nejnovějšího“. Zpráva se tvoří jen po akci uživatele, nikdy při načtení stránky. Způsob oznamování popisuje i článek Live regiony a notifikace prakticky: aria-live, aria-atomic, aria-relevant a aria-busy.
Proč to takhle je
- U tabulky z prvků
<div>musí být role v kódu, protože žádný z těch prvků sémantiku tabulky sám nenese, a proto ji ani změnadisplayneodebere. - Každý údaj je v kódu jen jednou, protože odečítač čte všechno, co není skryté přes
display:none,hiddenneboaria-hidden. Třídavisually-hiddenskrývá jen vizuálně. - Přeskládání dělá mřížka přes
grid-area, takže pořadí v kódu zůstává stejné a nevzniká druhá kopie dat. - Popisek přes
::beforemá prázdný alternativní text, protože odečítač ho už zná z nadpisu sloupce. - Změna řazení se oznamuje ručně, protože
aria-sortje na rodiči tlačítka a čtečky jeho změnu po stisku nehlásí.
Pravidla
- První pravidlo ARIA říká, že kde existuje nativní prvek se správnou sémantikou, použije se ten, a role se přidávají jen tam, kde to jinak nejde.
- WCAG 1.3.1 (Informace a vztahy) vyžaduje, aby vztah mezi nadpisem sloupce a hodnotou buňky zůstal zachovaný i po změně vizuálního rozvržení.
- WCAG 1.3.2 (Smysluplná posloupnost) vyžaduje, aby pořadí obsahu v kódu zůstalo logické i po přeskládání pomocí CSS.
- WCAG 1.4.10 (Reflow) požaduje, aby šel obsah číst bez posouvání ve dvou směrech najednou.
- WCAG 4.1.3 (Stavové zprávy) vyžaduje, aby se změny stavu, například nové řazení, oznámily odečítači bez přesunu fokusu.