Toegankelijke keyboard-navigatie en focusbeheer — praktische implementatie
Keyboard-toegankelijkheid en correct focusbeheer gaan in de praktijk vaak mis: verkeerde tabindex-waardes, geen zichtbare focus-stijlen, gefocuste elementen die verdwijnen of verkeerd focusherstel na modals zijn veelvoorkomende fouten. Dat leidt tot gebrekkige navigeerbaarheid voor mensen die geen muis gebruiken en tot afkeuring bij WCAG-audits.
Wij lossen dit op met concrete, testbare patterns: semantische HTML, duidelijke tabindex-regels, CSS :focus-visible, toegankelijke modals en focus-traps, plus kant-en-klare JS-snippets om fouten direct te repareren. Test direct met onze WCAG checker (wcagtool.nl/checker), installeer onze plugin (wcagtool.nl/plugin) en neem contact op via ons formulier (wcagtool.nl/contact) — we reageren binnen 24 uur.
Het probleem in de praktijk
Veelvoorkomende fouten
- Interactie-elementen zonder focusable attribute (div/span in plaats van button/a)
- Onnodig gebruik van tabindex=”0″ of tabindex=”-1″ dat de tabvolgorde breekt
- Geen of onduidelijke focus-styles; outline: none zonder alternatief
- Modals zonder focus-trap en zonder focusherstel bij sluiten
- Custom controls zonder ARIA/keyboard support (bijv. custom dropdowns die niet op Enter/Space reageren)
Waarom dit faalt bij tests
Automated tools detecteren vaak structurele fouten, maar het echte probleem toont zich bij manuele keyboard-tests en screenreader-sessies: verkeerde tabvolgorde, onbereikbare acties en verloren context.
Zo los je dit op in code
Gebruik semantische HTML eerst
Stap 1: vervang non-interactive elementen door native controls. Buttons en links zijn keyboard-focusable en hebben standaard assistive technologie ondersteuning.
<!-- FOUT -->
<div role="button" onclick="doAction()">Actie</div>
<!-- GOED -->
<button type="button" onclick="doAction()">Actie</button>
Zichtbare focus-styles: gebruik :focus-visible
Stap 2: geen outline: none zonder alternatief. Gebruik :focus-visible voor toegankelijke en visuele consistentie. Voor legacy browsers fallback.
/* Basis focus-style */
:focus-visible { outline: 3px solid #005fcc; outline-offset: 2px; border-radius: 4px; }
/* Fallback voor browsers zonder :focus-visible */
:focus { outline: 3px solid rgba(0,95,204,0.6); }
Tabindex-regels — houd het simpel
Stap 3: Regels:
- Gebruik tabindex alleen als het echt moet.
- tabindex=”0″ maakt element focusable in tabvolgorde — gebruik dit voor custom controls die native niet focusable zijn.
- tabindex=”-1″ om programmatic focus te ondersteunen (bijv. focus naar een element met JS), niet voor navigatie.
<div role="button" tabindex="0" aria-pressed="false" id="customBtn">Toggle</div>
<script>
document.getElementById('customBtn').addEventListener('keydown', function(e){
if(e.key === ' ' || e.key === 'Enter'){ e.preventDefault(); this.click(); }
});
</script>
Toegankelijke modal: focus-trap en focusherstel
Stap-voor-stap implementatie van een eenvoudige, toegankelijke modal met focus-trap en focusherstel.
<!-- HTML -->
<button id="openModal">Open modal</button>
<div id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
<div class="dialog">
<h2 id="modalTitle">Modal titel</h2>
<button id="closeModal">Close</button>
<a href="#">Link in modal</a>
</div>
</div>
<!-- JavaScript -->
<script>
const openBtn = document.getElementById('openModal');
const modal = document.getElementById('modal');
const closeBtn = document.getElementById('closeModal');
let lastFocused;
function trapFocus(container){
const focusable = container.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])');
const first = focusable[0];
const last = focusable[focusable.length -1];
container.addEventListener('keydown', function(e){
if(e.key !== 'Tab') return;
if(e.shiftKey && document.activeElement === first){ e.preventDefault(); last.focus(); }
else if(!e.shiftKey && document.activeElement === last){ e.preventDefault(); first.focus(); }
});
}
openBtn.addEventListener('click', () => {
lastFocused = document.activeElement;
modal.removeAttribute('hidden');
document.body.style.overflow = 'hidden';
trapFocus(modal);
modal.querySelector('[tabindex], button, a, input, select, textarea')?.focus();
});
closeBtn.addEventListener('click', () => {
modal.setAttribute('hidden', '');
document.body.style.overflow = '';
lastFocused?.focus();
});
</script>
Custom controls: ARIA + keyboard events
Voor custom widgets (dropdowns, sliders) combineer role/aria met keyboard handlers. Voorbeeld eenvoudige custom dropdown:
<div class="dropdown" role="listbox" tabindex="0" aria-activedescendant="opt1" id="dd">
<div role="option" id="opt1" data-value="1">Optie 1</div>
<div role="option" id="opt2" data-value="2">Optie 2</div>
</div>
<script>
const dd = document.getElementById('dd');
let current = 0;
const options = Array.from(dd.querySelectorAll('[role="option"]'));
function updateActive(i){
options.forEach((o, idx) => {
o.setAttribute('aria-selected', idx===i);
});
dd.setAttribute('aria-activedescendant', options[i].id);
options[i].focus();
}
dd.addEventListener('keydown', e => {
if(e.key === 'ArrowDown'){ e.preventDefault(); current = Math.min(current+1, options.length-1); updateActive(current); }
if(e.key === 'ArrowUp'){ e.preventDefault(); current = Math.max(current-1, 0); updateActive(current); }
if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); console.log('selected', options[current].dataset.value); }
});
</script>
Checklist voor developers
- Gebruik semantische HTML voor interactieve elementen (buttons, links, inputs).
- Verwijder onnodige tabindex-attributen en documenteer waar je tabindex gebruikt.
- Zorg voor zichtbare focus-styles (gebruik :focus-visible).
- Implementatie van modals: aria-modal, role=”dialog”, focus-trap en focusherstel.
- Custom components: voeg correcte role/aria-* attributen toe en implementeer Enter/Space/Arrow keyboard gedrag.
- Test met alleen toetsenbord en met screenreader (NVDA/VoiceOver).
- Run onze WCAG checker: wcagtool.nl/checker en gebruik onze browser-plugin: wcagtool.nl/plugin.
Tips voor designers en redacties
Design tokens voor focus
Maak focus-styles onderdeel van je design system via tokens. Definieer kleur, dikte en offset zodat developers consistentie kunnen toepassen.
Content-structuur en tabvolgorde
Zorg dat redacties logisch opdelen van content: koppen, paragrafen en links in een inhoudslogische volgorde voorkomen verwarring bij tab-navigatie. Vermijd te veel interactieve elementen in één blok.
Formulierlabels en foutmeldingen
Redacties: gebruik duidelijke labels en help-tekst. Koppel foutmeldingen met aria-describedby en focus naar het foutveld na submit.
<label for="email">E-mail</label>
<input id="email" name="email" type="email" aria-describedby="emailHelp" />
<div id="emailHelp">Gebruik een geldig e-mailadres</div>
Hoe test je dit?
Manuele keyboard-test (stap-voor-stap)
- Schakel muis uit of leg je hand op je toetsenbord.
- Tab door de pagina: elk interactief element moet logisch, voorspelbaar en zichtbaar focusbaar zijn.
- Shift+Tab controleert reverse volgorde.
- Voor modals: open modal, controleer dat focus binnen modal blijft (tab) en dat sluiten focus terugzet naar opener.
Screenreader-test
- NVDA (Windows): start NVDA, navigeer met Tab en toets NVDA+T of gebruik de tabbladen; controleer of aria-roles/labels kloppen.
- VoiceOver (macOS): schakel VoiceOver in, test keyboard-navigatie en controleer dat labels en status (expanded/selected) worden voorgelezen.
Automated tools + onze checker
Run onze WCAG checker: wcagtool.nl/checker en de browser-plugin (wcagtool.nl/plugin) voor snelle resultaten. Combineer met Axe en Lighthouse. Gebruik resultaten om gerichte fixes te implementeren en opnieuw te testen.
Test-cases om direct uit te voeren
- Open homepage, tab door header en nav; noteer of order logisch is.
- Open alle modals en dialogen; controleer focus-trap en herstel.
- Controleer alle custom controls op Enter/Space/Arrow support en dat aria-attributes up-to-date zijn.
- Gebruik onze checker en stuur de rapportlink naar ons support via wcagtool.nl/contact voor hulp (antwoord binnen 24 uur).
Praktische tip: voer deze mini-scan als onderdeel van je CI/CD pipeline met onze plugin en blokkeer deployments met regressies in keyboard-toegankelijkheid. Download de plugin: wcagtool.nl/plugin en test nu: wcagtool.nl/checker.
Laatste praktische code-check (plakbaar): focus-herstel helper
function openDialog(modal, opener){
opener = opener || document.activeElement;
modal.removeAttribute('hidden');
modal.setAttribute('aria-hidden', 'false');
const focusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');
focusable?.focus();
function closeHandler(){ modal.setAttribute('hidden',''); modal.setAttribute('aria-hidden','true'); opener?.focus(); modal.removeEventListener('dialog:close', closeHandler); }
modal.addEventListener('dialog:close', closeHandler);
return () => modal.dispatchEvent(new Event('dialog:close'));
}
Test je website meteen met onze WCAG checker (wcagtool.nl/checker). Heb je vragen of wil je hulp bij implementatie? Vul het contactformulier: wcagtool.nl/contact — we antwoorden binnen 24 uur.