Keyboard en focus management: praktisch toepassen
Keyboard-toegankelijkheid en juist focusbeheer falen vaak in productie omdat componenten visueel kloppen maar niet functioneel via toetsen bedienbaar zijn. Ontwikkelaars gebruiken niet-semantische elementen, designers verbergen focus-styles en modals breken focus-traps: resultaat is onbruikbare interfaces voor toetsenbordgebruikers.
Wij lossen dit op met concrete patterns, getestbare code-snippets en checklists die je direct kunt implementeren. Test je site meteen met onze WCAG checker of download onze plugin voor snelle feedback; vragen? Gebruik het contactformulier — we reageren binnen 24 uur.
Het probleem in de praktijk
Veelvoorkomende fouten
- Interactieve elementen zijn geen echte buttons/links (div/span zonder role/keyboard-support).
- Geen zichtbare focus (outline: none zonder alternatief).
- Modals en dialogs breken focus flow, geen terugzetten van focus na sluiten.
- Custom widgets missen ARIA-attributes en keyboard-handler voor Enter/Escape/Arrow.
- Tabindex misbruik: negatieve tabindex voor interactieve items of teveel tabindex=”1/2″.
Waarom dit impact heeft op WCAG
WCAG 2.1 vereist toetsenbord-toegankelijkheid (2.1.1), focus-visible (2.4.7) en navigatie (2.4.*). Fouten hierboven leiden direct tot niet voldoen aan die succescriteria.
Zo los je dit op in code
Basisregel: gebruik semantische HTML
Gebruik button, a en input waar mogelijk. Pas ARIA alleen toe om semantics aan te vullen, niet te vervangen.
<!-- Goede keuze: native button is keyboard-ready en toegankelijk --><br><button type="button" class="btn" id="saveBtn">Opslaan</button>
Skip-link: verplicht voor lange pagina’s
Voeg een skip-link toe die zichtbaar wordt bij focus zodat toetsenbordgebruikers snel naar content kunnen springen.
<a href="#main" class="skip-link">Sla naar hoofdinhoud over</a><br><!-- CSS --><br>.skip-link{position:absolute;left:-999px;top:auto;width:1px;height:1px;overflow:hidden}<br>.skip-link:focus{position:static;left:auto;width:auto;height:auto;clip:none;padding:8px;background:#000;color:#fff;z-index:1000}
Focus-styles: gebruik :focus-visible
Verwijder nooit outlines zonder alternatief. Gebruik :focus-visible om visuele rommel te beperken maar behoud zichtbare focus.
/* zichtbare focus */<br>:focus-visible{outline:3px solid #005fcc;outline-offset:2px;border-radius:2px}
Accessible modal / dialog pattern
Belangrijk: aria-modal, role=”dialog”, focus trap bij openen en restore focus bij sluiten.
<!-- HTML --><br><button id="openModal">Open dialog</button><br><div id="dialog" role="dialog" aria-modal="true" aria-hidden="true" tabindex="-1"><br> <button id="closeModal">Sluiten</button><br> <div>Inhoud van modal</div><br></div><br><!-- JavaScript: simpel focus trap & restore --><br>const openBtn = document.getElementById('openModal');<br>const dialog = document.getElementById('dialog');<br>const closeBtn = document.getElementById('closeModal');<br>let lastFocused = null;<br>function openDialog(){ lastFocused = document.activeElement; dialog.setAttribute('aria-hidden','false'); dialog.style.display='block'; dialog.focus(); document.addEventListener('keydown',trapTab); document.addEventListener('keydown',handleEsc);} <br>function closeDialog(){ dialog.setAttribute('aria-hidden','true'); dialog.style.display='none'; document.removeEventListener('keydown',trapTab); document.removeEventListener('keydown',handleEsc); if(lastFocused) lastFocused.focus(); } <br>function handleEsc(e){ if(e.key==='Escape'){ closeDialog(); }} <br>function trapTab(e){ if(e.key!=='Tab') return; const focusable = dialog.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex=\"-1\"])'); const first = focusable[0]; const 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(); }} <br>openBtn.addEventListener('click',openDialog); closeBtn.addEventListener('click',closeDialog);
Custom interactive widgets: voorbeeld dropdown
Als je geen native select gebruikt, implementeer keyboard-bediening en ARIA attributes.
<!-- minimal accessible dropdown --><br><div class="dropdown" role="listbox" tabindex="0" aria-labelledby="label" aria-expanded="false"><br> <div id="label">Kies optie</div><br> <ul role="presentation" class="options" hidden><br> <li role="option" tabindex="-1">Optie 1</li><br> <li role="option" tabindex="-1">Optie 2</li><br> <li role="option" tabindex="-1">Optie 3</li><br> </ul><br></div><br><!-- JS: open/close en keyboard navigation --><br>const dd = document.querySelector('.dropdown'); const options = dd.querySelectorAll('[role=\"option\"]'); let idx = -1; dd.addEventListener('keydown',e => { if(e.key==='Enter' || e.key===' '){ const expanded = dd.getAttribute('aria-expanded')==='true'; dd.setAttribute('aria-expanded', String(!expanded)); dd.querySelector('.options').hidden = expanded; if(!expanded){ idx=0; options[idx].focus(); } e.preventDefault(); } else if(e.key==='ArrowDown'){ idx = Math.min(idx+1, options.length-1); options[idx].focus(); e.preventDefault(); } else if(e.key==='ArrowUp'){ idx = Math.max(idx-1,0); options[idx].focus(); e.preventDefault(); } else if(e.key==='Escape'){ dd.setAttribute('aria-expanded','false'); dd.querySelector('.options').hidden = true; dd.focus(); } });
Checklist voor developers
- Gebruik semantische controls (button/a/input) waar mogelijk.
- Zorg voor visible focus met :focus-visible; verwijder outline nooit zonder alternatief.
- Voeg skip-link toe en test met keyboard-only.
- Implementeer focus trap voor modals en restore focus na sluiten.
- Voor custom components: voeg ARIA roles, aria-expanded, aria-hidden, aria-selected etc. toe en implementeer Enter/Space/Arrow/Escape handlers.
- Gebruik tabindex=”0″ om niet-focusable elementen focusable te maken; gebruik tabindex=”-1″ alleen voor script-managed focus.
- Test met screenreader + keyboard en met onze WCAG checker/validator (https://wcagtool.nl/validator).
Tips voor designers en redacties
Design-systeem regels
- Documenteer focus-states in het design system (kleur, grootte, offset).
- Vermijd hover-only interacties; zorg voor keyboard alternatieven.
- Voor content-editors: geen inline elementen als knoppen; gebruik CMS-componenten die semantisch juiste markup uitgeven.
Redactionele richtlijnen
- Zorg dat links tekst bevatten die betekenis geeft buiten context (geen “klik hier”).
- Gebruik kopstructuur (h1-h6) logisch; dat verbetert keyboard-navigation en screenreader-ervaring.
Hoe test je dit?
Handmatige tests (snel en effectief)
- Schakel muis weg: navigeer alleen met Tab/Shift+Tab en toets Enter/Space. Alle interactieve items moeten bereikbaar en bedienbaar zijn.
- Controleer focus volgorde: logische, voorspelbare volgorde van elementen.
- Open modals, probeer Tab en Shift+Tab om te verifiëren dat focus niet buiten de modal gaat.
- Test met Escape: modals en overlays moeten sluiten en focus moet teruggaan naar de trigger.
- Controleer zichtbare focus op alle controles (ook custom components).
Automated checks en tools
Gebruik onze WCAG checker/validator (https://wcagtool.nl/validator) voor snelle scans. Daarnaast: axe-core, Lighthouse en WAVE voor aanvullende fouten. Integreer onze plugin (https://wcagtool.nl/plugin) in je workflow voor real-time feedback.
Screenreader tests
Test met NVDA/JAWS (Windows) en VoiceOver (macOS/iOS). Controleer dat labels worden voorgelezen en dat aria-states correct veranderen bij interactie.
Concrete mini-how-to’s
1) Maak een custom button toegankelijk
<!-- liever gebruik native button, maar zo maak je een div of span toegankelijk --><br><div role="button" tabindex="0" aria-pressed="false" id="myBtn">Schakel</div><br>const el = document.getElementById('myBtn'); el.addEventListener('click',toggle); el.addEventListener('keydown',e => { if(e.key==='Enter' || e.key===' '){ e.preventDefault(); toggle(); }}); function toggle(){ const pressed = el.getAttribute('aria-pressed')==='true'; el.setAttribute('aria-pressed', String(!pressed)); }
2) Restore focus na navigatie away / modal sluiten
// bij open: lastFocused = document.activeElement; // bij close: if(lastFocused) lastFocused.focus();
3) Vermijd tabindex-waarden >0
Gebruik nooit tabindex=”1″ of meer; dat breekt natuurlijke tab-volgorde. Zet focusable items op tabindex=”0″.
Belangrijke testscenario’s die je direct draait
- Keyboard-only: voltooi het primaire taakpad (zoek, selecteer, invullen, opslaan).
- Modal: open, navigeer, sluit met Escape en controleer terugzetten focus.
- Custom widgets: navigeer via Arrow-keys en activeer opties met Enter/Space.
- Focus zichtbaarheid: controleer contrast van focus-indicator en dat deze niet onzichtbaar is.
Calls-to-action
Test je pagina nu met onze WCAG checker: <a href=”https://wcagtool.nl/validator”>https://wcagtool.nl/validator</a>. Wil je realtime feedback in je browser? Download onze plugin: <a href=”https://wcagtool.nl/plugin”>https://wcagtool.nl/plugin</a>. Vragen over implementatie? Stuur ze via ons contactformulier (<a href=”https://wcagtool.nl/contact”>https://wcagtool.nl/contact</a>) — we antwoorden binnen 24 uur.
Laatste praktische tip
Voeg dit korte test-script toe aan je checklist en draai het na elke release: open alle modals, navigeer keyboard-only door alle interactieve componenten en verifieer dat focus nooit verdwijnt. Gebruik daarnaast direct onze validator (https://wcagtool.nl/validator) voor een snelle automatische controle.