Práce s focusem
Uživatel klávesnice a čtečky obrazovky pracuje vždy na jednom místě stránky, tam, kde je právě fokus. Když se po stisknutí tlačítka změní obsah jinde, třeba se načtou další položky, zobrazí se chyba nebo zmizí prvek, na kterém uživatel stál, vidoucí si změny všimne okamžitě. Uživatel čtečky o ní ale bez pomoci stránky nemusí vědět vůbec. Stránka ho proto musí upozornit, buď přesunem fokusu na nový obsah, nebo stavovou zprávou.
1. Kdy fokus přesunout a kdy stačí zpráva
Přesun fokusu zasahuje do toho, kde uživatel právě je, takže se nedělá po každé změně. Fokus se přesouvá tehdy, když uživatel po své akci potřebuje s novým obsahem pracovat, nebo když by o své místo přišel. Jinak stačí o změně dát vědět.
- Fokus se přesune, když se otevře dialog, když se po odeslání formuláře zobrazí souhrn chyb, nebo když zmizí prvek, na kterém fokus byl (část 4).
- Stačí stavová zpráva, když uživatel pokračuje tam, kde byl, a nový obsah jen doplňuje jeho akci, třeba počet načtených položek, potvrzení uložení nebo počet výsledků hledání (část 3).
- Fokus zůstane na tlačítku, když se text jen rozbalí. Má následovat hned za tlačítkem (část 2), čtečka ho přečte jako další obsah a stav panelu sděluje
aria-expanded, jak je popsáno v části Přístupný accordion a rozbalovací panely. - Pokračování od cíle zařídí prohlížeč, když uživatel použije přeskakovací odkaz nebo odkaz na kotvu v téže stránce (část 5).
2. Tlačítko rozbalí text: kam ho umístit
U jednoduchého rozbalení, tedy tlačítka, které jen odkryje další text, se fokus nepřesouvá. Adrian Roselli v textu o rozbalovacích prvcích píše, že tyto prvky ve výchozím stavu fokus na zobrazený obsah nepřesouvají. Důvod je praktický. Když uživatel tlačítko stiskne omylem, zůstane na místě a může stejným stiskem akci hned vrátit. Rozbalený obsah má navíc v kódu bezprostředně následovat za ovládacím prvkem, aby čtečka mohla pokračovat v čtení a uživatel klávesnice se hned dostal k případným ovládacím prvkům v něm.
Správný vzor je tedy tlačítko a hned za ním panel:
<button type="button" id="tlacitko" aria-expanded="false" aria-controls="podrobnosti">
Podrobnosti objednávky
</button>
<div id="podrobnosti" hidden>
<h3>Podrobnosti objednávky</h3>
<p>Objednávku jsme odeslali dopravcem ve čtvrtek…</p>
</div>
const tlacitko = document.getElementById('tlacitko');
const panel = document.getElementById('podrobnosti');
tlacitko.addEventListener('click', () => {
const otevreno = tlacitko.getAttribute('aria-expanded') === 'true';
tlacitko.setAttribute('aria-expanded', String(!otevreno));
panel.hidden = otevreno;
});
Čtečka po stisknutí ohlásí „rozbaleno“ a uživatel pokračuje šipkou dolů přímo do rozbaleného textu. Název tlačítka se při rozbalení nemění. Kdyby se přepisoval text z „Zobrazit“ na „Skrýt“, čtečka by změnu názvu i změnu stavu ohlásila dvakrát.
Někdy to ale rozvržení nedovolí, třeba když tlačítko tvoří spodní okraj karty a text se má objevit nad ním. To je méně vhodný vzor. Když text v kódu předchází tlačítku, uživatel stojí na tlačítku a nový text je před ním, tedy v části stránky, kterou už čtečka přečetla. Po stisknutí uslyší jen, že je tlačítko rozbaleno, a o textu nad sebou se nedozví. Při zavření se navíc text nad tlačítkem odebere a čtecí kurzor se může ocitnout o několik řádků níže. Aspoň stavová zpráva typu „Podrobnosti se rozbalily nad tlačítkem“ uživateli řekne, kam se text objevil, ale pořadí v dokumentu nespraví ani čtecí kurzor nepřesune, takže je to jen náplast. V ukázce ji lze zapnout i vypnout. Varianta 2 z ukázky se od správného vzoru liší jen pořadím v kódu, kde panel předchází tlačítku, a volitelnou zprávou:
<div id="podrobnosti" hidden>
<h3>Podrobnosti objednávky</h3>
<p>Objednávku jsme odeslali dopravcem ve čtvrtek…</p>
</div>
<button type="button" id="tlacitko" aria-expanded="false" aria-controls="podrobnosti">
Podrobnosti objednávky
</button>
<p id="stav" role="status" aria-live="polite"></p>
tlacitko.addEventListener('click', () => {
const otevreno = tlacitko.getAttribute('aria-expanded') === 'true';
tlacitko.setAttribute('aria-expanded', String(!otevreno));
panel.hidden = otevreno;
// Volitelná stavová zpráva, jen náplast pro text nad tlačítkem.
stav.textContent = otevreno
? 'Podrobnosti nad tlačítkem se sbalily.'
: 'Podrobnosti se rozbalily nad tlačítkem.';
});
Přesun fokusu na rozbalený text to spolehlivě nespraví. Při zkoušení s NVDA uživatel sice uslyší nový text, ale ztratí představu o tom, kde na stránce je, a po zavření panelu se problém s posunutým čtecím kurzorem vrací. Spolehlivé řešení je neumisťovat text nad tlačítko, ale hned za něj. Pokud ho chcete mít vizuálně nad tlačítkem, narážíte na rozpor mezi pořadím v kódu a na obrazovce, který popisuje část Pořadí prvků v kódu a vizuálně.
3. Načtení dalších položek: fokus, nebo stavová zpráva
Tlačítko „Načíst další“ přidá položky na konec seznamu, tedy nad samotné tlačítko. Uživatel stojí na tlačítku, takže nové položky jsou v pořadí čtení před ním a čtečka je sama nepřečte. Existují dvě rozumné cesty a záleží na tom, co uživatel s novými položkami dělá. Doporučení se liší. Někdo přesouvá fokus na první novou položku, jiný nechává fokus na tlačítku a hlásí jen stavovou zprávu, takže rozhodnutí je třeba ověřit na čtečce.
Když chce v seznamu pokračovat, přesune se fokus na první novou položku. Položka není interaktivní, proto potřebuje tabindex="-1":
const prvniNova = seznam.children[puvodniPocet];
prvniNova.setAttribute('tabindex', '-1');
prvniNova.focus();
Když uživatel zůstává u tlačítka a nové položky jen přibývají, stačí stavová zpráva. Element s ARIA rolí status je takzvaný live region, čtečka tedy ohlásí každou změnu jeho textu, aniž by se fokus pohnul:
<p id="stav" role="status" aria-live="polite"></p>
stav.textContent = 'Načteno dalších 5 zpráv, celkem 10.';
Element s rolí status musí být v kódu stránky už při jejím načtení, ještě prázdný, jinak čtečka první změnu textu nemusí ohlásit. Redundantní aria-live="polite" zvyšuje kompatibilitu s různými čtečkami. Obě cesty spíš nespojujte. MDN u role status výslovně uvádí, že když je potřeba fokus přesunout, není stavová zpráva zpravidla vhodná. Pokud je přesto spojíte, počítejte s tím, že čtečka oznámí položku s focusem i zprávu a uživatel může slyšet totéž dvakrát.
Pozor na tlačítko samotné. Jakmile jsou načteny všechny položky, nesmíte tlačítko s focusem odstranit ani mu dát atribut disabled, protože by o fokus přišlo. Ponechte ho v dokumentu s aria-disabled="true", čtečka ho ohlásí jako nedostupné a fokus zůstane na místě. Stisk Enteru nebo mezerníku ale akci pořád spustí, proto ji musíte v obsluze kliknutí sami zablokovat:
tlacitko.addEventListener('click', () => {
if (tlacitko.getAttribute('aria-disabled') === 'true') {
return; // aria-disabled samo nic nezablokuje
}
// … načtení a přidání dalších položek …
if (seznam.children.length >= maximum) {
tlacitko.setAttribute('aria-disabled', 'true');
}
});
4. Smazání prvku, na kterém je fokus
Nejčastěji se fokus ztrácí tehdy, když uživatel stiskne tlačítko „Smazat“ a prvek, na kterém byl, z dokumentu zmizí. Prohlížeč pak fokus vrátí na <body>. Klávesa Tab sice v dnešních prohlížečích pokračuje od místa, kde prvek byl, ale fokus už není na žádném ovládacím prvku a uživatel čtečky může přijít o své místo v seznamu.
Cíl fokusu se proto určí předem. Focus dostane další položka seznamu, a když žádná další není, tak předchozí. Je-li seznam po smazání prázdný, dostane fokus zpráva o prázdném seznamu:
function smaz(li) {
// Soused se zjistí PŘED odstraněním, potom už ho z prvku nezískáte.
const soused = li.nextElementSibling || li.previousElementSibling;
li.remove();
stav.textContent = 'Položka byla smazána.';
if (soused) {
soused.querySelector('button').focus();
} else {
prazdno.hidden = false;
prazdno.focus(); // <p id="prazdno" tabindex="-1">Seznam je prázdný.</p>
}
}
Tady se vyplatí přidat i stavovou zprávu typu „Položka byla smazána“, jak ukazuje kód výše. Varianta „fokus se ztratí“ z ukázky je tentýž kód bez volání focus(). Čtečka totiž u tlačítka sousední položky s focusem jen přečte název položky, ale nepoví, že předchozí akce proběhla.
5. Přeskakovací odkazy: kam pokračuje Tab
Přeskakovací odkaz na začátku stránky, třeba „Přejít na menu“ nebo „Přejít na hlavní obsah“, je zvláštní případ. Je to první tabstop stránky, tedy první místo, na kterém se focus při procházení klávesou Tab zastaví. Uživateli klávesnice umožní vynechat opakující se hlavičku a navigaci, což požaduje WCAG 2.4.1 (Vynechání bloků). O tom, odkud bude Tab pokračovat, tady nerozhoduje skript, ale sám prohlížeč, protože odkaz vede na identifikátor v téže stránce:
<a class="preskocit" href="#menu">Přejít na menu</a>
<a class="preskocit" href="#obsah">Přejít na hlavní obsah</a>
…
<div id="menu">…odkazy navigace…</div>
<div id="obsah">…hlavní obsah…</div>
.preskocit {
position: absolute;
left: -9999px;
}
.preskocit:focus {
left: 8px;
}
Odkazy musí být v kódu první, jinak je uživatel klávesnice minul, a při focusu musí být vidět. Skrývají se proto posunutím mimo obrazovku, ne přes display: none, protože takto skrytý odkaz focus nepřijme. Hodí se i druhý odkaz přímo na menu, když je navigace dlouhá.
Zbývá cíl odkazu. Tradiční rada říká dát cíli tabindex="-1", aby na něj prohlížeč přesunul focus. Manuel Matuzović ale v roce 2026 vyzkoušel přeskakovací odkazy v Chrome, Firefoxu, Edge a Safari a se čtečkami včetně NVDA a VoiceOveru a došel k tomu, že tabindex="-1" dnes není nutný. Po přechodu na identifikátor prohlížeč nastaví místo, odkud klávesa Tab dál hledá, takže další stisk Tabu vede do obsahu za odkazem, i když cíl focus nedostal. Podle jeho článku může navíc tabindex="-1" na prvku main na iOS rozbít tlačítko zpět. Cíl bez focusu ovšem nemá rámeček, takže se dá označit selektorem :target. U VoiceOveru autor zmiňuje výjimku: po stisku Enteru místo VO+Mezerník zůstane focus na odkazu. Zdroje se tedy liší a nejjistější je přeskakovací odkaz vyzkoušet na vlastní stránce. Obě varianty z ukázky se liší jen takto:
<!-- tradiční varianta: cíl dostane i focus -->
<div id="obsah" tabindex="-1">…hlavní obsah…</div>
<!-- varianta bez tabindex: cíl focus nedostane, jen určí, odkud pokračuje Tab -->
<div id="obsah">…hlavní obsah…</div>
/* cíl bez focusu nemá rámeček, proto ho zvýrazní :target */
#obsah:target {
outline: 3px dashed;
}
6. Jak dát prvku focus správně
- Prvek musí přijmout focus. Nativně ho přijímají odkazy, tlačítka a formulářová pole. Ostatní prvky, tedy nadpis, odstavec nebo položka seznamu, potřebují
tabindex="-1". - Nepoužívejte
tabindex="0"na neinteraktivních prvcích. Prvek by se stal tabstopem a uživatel by na něm zbytečně stavěl, přestože se s ním nedá nic dělat. MDN doporučuje používat jen hodnoty0a-1. focus()zavolejte až po zobrazení prvku. Prvek, který je ještěhiddennebo mádisplay: none, fokus nepřijme. Nejdřív tedy odkryjte panel a teprve potom zavolejtefocus().- Prvek s focusem musí mít srozumitelný obsah. Čtečka přečte jeho název nebo text, takže nadpis, odstavec nebo tlačítko s popiskem poslouží, prázdný
<div>ne. - Rámeček fokusu musí být vidět. Nepřepisujte
outlinenanonea ponechte nebo definujte výraznou variantu v:focus-visible. - Přilepená hlavička nesmí zakrýt prvek s focusem. Pokud ji stránka má, nastavte rolovanému kontejneru, obvykle prvku
html,scroll-padding-topalespoň o výšku hlavičky, jak doporučuje technika C43 u pravidla WCAG 2.4.11.
Proč to takhle je
- Fokus je pro uživatele čtečky jediné místo, kde se na stránce nachází, protože čtečka čte od něj. Změna mimo toto místo je proto neviditelná, dokud ji stránka neoznámí nebo neposune uživatele k ní.
- U jednoduchého rozbalení se fokus nepřesouvá, protože při omylu uživatel zůstane na místě a může akci hned vrátit. Přesun fokusu se vyhrazuje pro případy, kdy uživatel s novým obsahem hned pracuje, nebo kdy by o své místo přišel.
- Přesun fokusu a stavová zpráva říkají často totéž, takže jejich spojení může čtečku zahltit a je třeba ho ověřit.
- Cíl fokusu po smazání se určuje před odstraněním prvku proto, že odstraněný prvek už nemá sousedy a nic o svém okolí neví.
Pravidla
- WCAG 2.4.1 (Vynechání bloků) vyžaduje mechanismus, kterým jde obejít bloky obsahu opakující se na více stránkách, například přeskakovací odkaz.
- WCAG 2.4.3 (Pořadí fokusu) vyžaduje, aby pořadí, v jakém fokus prochází stránkou, zachovávalo smysl a ovladatelnost, což platí i po změně obsahu.
- WCAG 4.1.3 (Stavové zprávy) vyžaduje, aby šlo důležité změny stavu oznámit čtečce i bez přesunu fokusu.
- WCAG 2.4.7 (Fokus je viditelný) vyžaduje, aby byl prvek s focusem vždy vidět, i když se na něj fokus přesunul skriptem.
- WCAG 2.4.11 (Fokus není zakrytý, minimum) vyžaduje, aby prvek s focusem nebyl celý skrytý za přilepeným obsahem, například hlavičkou.
- WCAG 4.1.2 (Název, role, hodnota) vyžaduje, aby
aria-expandedvždy odpovídal skutečnému stavu rozbalovacího panelu.
Souhrnně: fokus přesouvejte tam, kde uživatel s novým obsahem pracuje nebo kde by o své místo přišel, jinak stačí stavová zpráva, a nikdy nenechte fokus zmizet spolu se smazaným prvkem.