Keyboard-toegankelijkheid & focusbeheer: praktisch toepassen
In de praktijk faalt toegankelijkheid vaak op keyboard-niveau: interactieve componenten zijn niet focusable, focusstijlen worden weggepoetst, of modals en custom controls breken de tabbeloop. Dat leidt tot onbereikbare interfaces voor toetsenbordgebruikers en screenreadergebruikers.
Wij vertalen WCAG-vereisten naar direct inzetbare oplossingen: compacte code-snippets, patterns voor modals, dropdowns en formulieren, plus testbare checklists. Test je site meteen met onze WCAG checker of installeer onze plugin voor geautomatiseerde detectie — en bij vragen helpen we binnen 24 uur via ons contactformulier.
Het probleem in de praktijk
Veelvoorkomende fouten
- Interactie-elementen zonder native semantics (div/button misbruik) of zonder tabindex/role.
- Focus indicatoren verwijderd via outline: none; waardoor gebruikers niet kunnen zien waar de focus is.
- Modals en dialogs zonder focus trap of zonder terugzetten van focus bij sluiten.
- Custom controls (dropdowns, tabs) zonder ARIA-attributes en keyboard-handlers.
- Skip-links of header structuren ontbreken, content-editors plaatsen content zonder logische heading-hiërarchie.
Waarom dat problemen oplevert
WCAG vereist dat functionaliteit toegankelijk is via toetsenbord (2.1.1) en dat focus visueel zichtbaar is (2.4.7 / 2.4.11 afhankelijk van context). Deze fouten leiden tot niet-conforme pagina’s en slechte gebruikerservaring.
Zo los je dit op in code
Basisprincipe: gebruik native controls altijd waar mogelijk
Prefer <button>, <a> met href en form-elementen boven generieke <div> of <span>. Native elementen brengen automatisch keyboard- en focusgedrag mee.
Skip-link: zet dit direct bovenaan je page
<a href="#maincontent" class="skip-link">Direct naar hoofdinhoud</a>
<!-- CSS -->
.skip-link{position:absolute;left:-999px;top:auto;width:1px;height:1px;overflow:hidden}
.skip-link:focus{position:static;left:0;top:0;width:auto;height:auto;padding:8px;background:#005a9c;color:#fff;z-index:9999}
Focusstijlen: maak ze zichtbaar en stijlbaar
/* Goede focusstijl, verwijder nooit alle outlines */
:focus{outline:3px solid #ffbf47;outline-offset:2px;border-radius:2px}
/* Of op specifieke elementen */
button:focus,a:focus,input:focus,select:focus,textarea:focus{box-shadow:0 0 0 3px rgba(0,90,156,0.18)}
Maak custom controls keyboard-navigable (dropdown voorbeeld)
Voorbeeld: een eenvoudige custom dropdown met ARIA en keyboard-handling.
<div class="dropdown" id="dd1">
<button id="dd1-toggle" aria-haspopup="listbox" aria-expanded="false">Kies optie</button>
<ul id="dd1-list" role="listbox" tabindex="-1" hidden>
<li role="option" data-value="1" tabindex="0">Optie 1</li>
<li role="option" data-value="2" tabindex="-1">Optie 2</li>
<li role="option" data-value="3" tabindex="-1">Optie 3</li>
</ul>
</div>
<script>
const toggle = document.getElementById('dd1-toggle');
const list = document.getElementById('dd1-list');
let options = Array.from(list.querySelectorAll('[role=\"option\"]'));
let focusedIndex = -1;
toggle.addEventListener('click', ()=>{const expanded = toggle.getAttribute('aria-expanded')==='true';toggle.setAttribute('aria-expanded', String(!expanded));list.hidden = expanded; if(!expanded){list.focus(); focusedIndex=0;options[0].tabIndex=0;options[0].focus();}});
toggle.addEventListener('keydown', e=>{if(e.key==='ArrowDown'){e.preventDefault();toggle.click();}});
list.addEventListener('keydown', e=>{
if(e.key==='ArrowDown'){e.preventDefault(); focusedIndex = Math.min(focusedIndex+1, options.length-1); options.forEach(o=>o.tabIndex=-1); options[focusedIndex].tabIndex=0; options[focusedIndex].focus();}
if(e.key==='ArrowUp'){e.preventDefault(); focusedIndex = Math.max(focusedIndex-1, 0); options.forEach(o=>o.tabIndex=-1); options[focusedIndex].tabIndex=0; options[focusedIndex].focus();}
if(e.key==='Enter' || e.key===' '){e.preventDefault(); selectOption(options[focusedIndex]);}
if(e.key==='Escape'){toggle.click(); toggle.focus();}
});
options.forEach((opt,i)=>{opt.addEventListener('click', ()=> selectOption(opt)); opt.addEventListener('focus', ()=>{focusedIndex=i;});});
function selectOption(opt){toggle.textContent = opt.textContent; toggle.setAttribute('data-value', opt.dataset.value); toggle.setAttribute('aria-expanded','false'); list.hidden = true; toggle.focus();}
</script>
Focus trap voor modals: minimale implementatie
Zorg dat focus binnen de modal blijft en dat focus teruggezet wordt naar de trigger na sluiten.
<button id="openModal">Open dialog</button>
<dialog id="modal">
<form method="dialog">
<p>Inhoud</p>
<button id="closeModal">Sluit</button>
</form>
</dialog>
<script>
const open = document.getElementById('openModal');
const dialog = document.getElementById('modal');
const close = document.getElementById('closeModal');
open.addEventListener('click', ()=>{dialog.showModal(); const focusable = dialog.querySelectorAll('a,button,input,select,textarea,[tabindex]:not([tabindex=\"-1\"])'); focusable[0].focus(); trapFocus(dialog);});
close.addEventListener('click', ()=>{dialog.close(); open.focus();});
function trapFocus(modal){
modal.addEventListener('keydown', function(e){
if(e.key !== 'Tab') return;
const focusable = Array.from(modal.querySelectorAll('a,button,input,select,textarea,[tabindex]:not([tabindex=\"-1\"])')).filter(el => !el.hasAttribute('disabled'));
if(focusable.length===0) return;
const first = focusable[0], last = focusable[focusable.length-1];
if(e.shiftKey && document.activeElement===first){ e.preventDefault(); last.focus(); }
else if(!e.shiftKey && document.activeElement===last){ e.preventDefault(); first.focus(); }
});
}
</script>
Formulieren: labels en duidelijke focus-volgorde
<label for="email">E-mail</label>
<input id="email" name="email" type="email" required aria-required="true">
<!-- Gebruik aria-describedby voor aanvullende instructie -->
<small id="emailHelp">We sturen alleen belangrijke berichten.</small>
<input aria-describedby="emailHelp">
Praktische ARIA-richtlijn: alleen als enhacement
Gebruik ARIA om semantics aan te vullen, nooit als vervanging van native elements. Bijvoorbeeld: gebruik role=”button” alleen als je geen <button> kunt gebruiken, en implementeer toetsenbordgedrag en aria-pressed/aria-expanded consistent.
Checklist voor developers
- Gebruik native controls tenzij strikt onvermijdelijk.
- Voeg zichtbare focusstijlen toe; verwijder outline: none niet zonder alternatief.
- Implementeer skip-links en logische heading-structuur (H1..H6).
- Zorg dat modals traps en return-focus hebben.
- Custom widgets: voeg role, aria-attributes, tabindex & keyboard handlers toe.
- Voer keyboard-only navigatie en screenreader-tests uit (zie testinstructies).
- Automatiseer checks met onze WCAG checker/validator en installeer de plugin voor CI-checks.
Tips voor designers en redacties
Designers — focus en zichtbaarheid
- Ontwerp voldoende contrast voor focus-indicatoren (3:1 tegen achtergrond als visuele aanwijzing).
- Maak component states (focus/hover/active) expliciet in de designbibliotheek.
- Documenteer keyboard-flows in PRD/component specs.
Redacties — contentstructuur en headings
- Gebruik logische heading-hiërarchie; elke pagina één H1.
- Voeg contextuele aria-describedby alleen toe als de extra tekst voor alle gebruikers relevant is.
- Vermijd inline styles die focusstijlen overschrijven; controleer CMS-templates op tabindex-uitroepen.
Hoe test je dit?
1. Handmatig: keyboard-only
- Schakel muis uit of leg je handen op je toetsenbord.
- Tab door de pagina: kan je alle interactieve items bereiken in logische volgorde?
- Gebruik Shift+Tab om terug te gaan en controleer focus volgorde.
- Open en sluit modals met Enter/Escape, navigeer in custom widgets met pijltjestoetsen.
2. Met screenreader
- Windows: NVDA (Insert+T voor titel check, Tab en Arrow voor navigatie).
- Mac: VoiceOver (Cmd+F5) — oefen navigatie tussen headings en links (VO+H en VO+K).
3. Browser tools en automated checks
- Use browser devtools Accessibility pane to inspect role/label/focusable properties.
- Lighthouse en axe-core integraties voor developer feedback.
- Run onze WCAG checker/validator op je pagina voor directe rapportage van keyboard- en ARIA-issues — test nu op https://wcagtool.nl of installeer onze browserplugin voor realtime feedback.
4. CI / E2E tests
Automatiseer keyboard-routes in Cypress/Playwright. Voorbeeld Playwright snippet die checkt dat modals focus terugzetten:
// Playwright (Node)
await page.click('#openModal');
await expect(page.locator('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.locator('#openModal')).toBeFocused();
Extra testcases die je direct kunt draaien
- Tab tot footer zonder focus-tegenvallers (assuming no trapped focus).
- Open navigation, navigeer met pijltjestoetsen in menulijsten.
- Form validatie: focus gaat naar eerste foutief veld met aria-invalid/aria-describedby.
Calls-to-action en ondersteuning
Test je pagina nu met onze gratis WCAG checker/validator op https://wcagtool.nl/checker. Wil je directe integratie? Download onze plugin op https://wcagtool.nl/plugin en voeg CI-checks toe. Vragen? Gebruik ons contactformulier op https://wcagtool.nl/contact — we beantwoorden binnen 24 uur.
Laatste praktische tip
Voeg deze globale helper toe aan je project om debugging van focus-issues tijdens ontwikkeling snel te detecteren:
// Development helper: force visible focus outlines
if(location.search.includes('show-focus')){const style=document.createElement('style');style.textContent=':focus{outline:3px solid rgba(255,191,71,0.95)!important;outline-offset:2px !important}';document.head.appendChild(style);console.info('Focus visuals forced');}
Test direct met onze tool: https://wcagtool.nl/checker — of installeer de plugin en stuur je vraag via het contactformulier; wij reageren binnen 24 uur.