HTML

Přístupné taby (tabs)

Tab, na který jde jen kliknout myší, bez správné obsluhy klávesnicí a bez potřebných rolí čtečce nic neřekne. Uživatel klávesnice se pak k jednotlivým tabům ani panelům nedostane a čtečka ohlásí jen obyčejná tlačítka bez souvislosti mezi nimi.

1. Základní kostra: tři role a jejich propojení

<div role="tablist" aria-label="Nastavení účtu">
    <button role="tab" id="obecne-tab" aria-selected="true" aria-controls="obecne-panel">Obecné</button>
    <button role="tab" id="zabezpeceni-tab" aria-selected="false" aria-controls="zabezpeceni-panel" tabindex="-1">Zabezpečení</button>
    <button role="tab" id="notifikace-tab" aria-selected="false" aria-controls="notifikace-panel" tabindex="-1">Notifikace</button>
</div>
<div role="tabpanel" id="obecne-panel" aria-labelledby="obecne-tab">Obsah obecného nastavení…</div>
<div role="tabpanel" id="zabezpeceni-panel" aria-labelledby="zabezpeceni-tab" hidden>Obsah zabezpečení…</div>
<div role="tabpanel" id="notifikace-panel" aria-labelledby="notifikace-tab" hidden>Obsah notifikací…</div>

Aktivní tab nemá tabindex napsaný vůbec, protože výchozí hodnota 0 mu stačí. Zbylé taby mají tabindex="-1", ať mezi nimi jde procházet jen šipkami, ne Tabem.

2. Přepínání tabu a řízení fokusu

const tablist = document.querySelector('[role="tablist"]');
const taby = Array.from(tablist.querySelectorAll('[role="tab"]'));

function vyberTab(tab) {
    taby.forEach(t => {
        const vybrano = t === tab;
        t.setAttribute('aria-selected', String(vybrano));
        t.tabIndex = vybrano ? 0 : -1;
        document.getElementById(t.getAttribute('aria-controls')).hidden = !vybrano;
    });
    tab.focus();
}

tablist.addEventListener('click', (e) => {
    const tab = e.target.closest('[role="tab"]');
    if (tab) vyberTab(tab);
});

vyberTab() mění aria-selected, tabindex i viditelnost panelu u všech tabů najednou, takže tyhle tři věci nikdy nezůstanou nesynchronizované.

3. Klávesová obsluha: šipky, Home, End

tablist.addEventListener('keydown', (e) => {
    const aktualni = taby.indexOf(document.activeElement);
    if (aktualni === -1) return;

    let dalsi = null;
    if (e.key === 'ArrowRight') {
        dalsi = taby[(aktualni + 1) % taby.length];
    } else if (e.key === 'ArrowLeft') {
        dalsi = taby[(aktualni - 1 + taby.length) % taby.length];
    } else if (e.key === 'Home') {
        dalsi = taby[0];
    } else if (e.key === 'End') {
        dalsi = taby[taby.length - 1];
    }

    if (dalsi) {
        e.preventDefault();
        vyberTab(dalsi);
    }
});

Šipky se u vodorovného seznamu točí dokola: po posledním tabu doprava se vrátí na první, po prvním doleva na poslední. U svislého seznamu s aria-orientation="vertical" nahrazují stejnou roli šipky nahoru a dolů.

4. Automatická vs. manuální aktivace

Ukázka výše rovnou přepíná obsah při pohybu šipkami, což je automatická aktivace, doporučená tam, kde se panel zobrazí bez znatelného zpoždění. Pokud načtení obsahu trvá déle, třeba jde o data z API, hodí se manuální aktivace: šipky jen přesouvají fokus po tabech, bez volání vyberTab(), a teprve Enter nebo mezerník obsah skutečně přepne.

tablist.addEventListener('keydown', (e) => {
    // ... stejná navigace šipkami jako výše, ale bez rovnou volaného vyberTab() ...
    if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        vyberTab(document.activeElement);
    }
});

Proč to takhle je

  • Jen aktivní tab má tabindex="0", zbylé mají -1. Mezi taby se totiž přechází šipkami, ne Tabem, takže by dvojí ovládání (Tab i šipky) mátlo, kudy se dostat dál.
  • Fokus po přepnutí šipkou zůstává na tabu, ne na panelu, aby uživatel klávesnice mohl hned pokračovat v procházení dalších tabů, aniž by musel skákat zpátky.
  • aria-selected a viditelnost panelu se mění spolu, protože jinak by čtečka a vizuální stav mluvily každý jinak.

Pravidla

  • WCAG 4.1.2 (Název, role, hodnota) — aria-selected musí vždy odpovídat tomu, který panel je právě vidět.
  • WCAG 2.1.1 (Klávesnice) — šipky, Home a End musí fungovat bez myši.
  • WCAG 2.4.3 (Pořadí zaměření) — roving tabindex udržuje fokus jen na jednom místě seznamu, ne na všech tabech najednou.

To je celé: tři role dají tabům tvar, klávesová obsluha šipkami přepíná fokus i obsah a aria-selected drží čtečku i vizuální stav v jednom.

Zdroje

Štítky