Keyboard accessibility & focus management — praktisch toepassen
Keyboard-toegankelijkheid en correct focusbeheer falen vaak in de praktijk omdat teams componenten bouwen zonder keyboard-events, verkeerde ARIA-toevoegingen of zonder duidelijke focusstijlen. Resultaat: interactieve elementen zijn onbereikbaar of onvoorspelbaar voor toetsenbordgebruikers en screenreadergebruikers.
Wij leggen stap-voor-stap uit hoe je components, formulieren en modals keyboard-proof maakt: semantic HTML, minimale en juiste ARIA, consistente tabindex-logica, zichtbare focus-stijlen en testbare procedures. Test direct met onze WCAG checker op wcagtool.nl/checker, download onze plugin op wcagtool.nl/plugin en bij vragen: contactformulier (antwoord binnen 24 uur).
Het probleem in de praktijk
Veelvoorkomende fouten:
- Custom controls zonder native semantics (div/span in plaats van button) zonder keyboard handlers of aria.
- Verkeerd gebruik van tabindex (tabindex=”0″ op te veel items of tabindex=”1″ etc.).
- Geen focus management bij modals, dialogs en dynamische content.
- Focus styles verwijderd door designers (outline: none) zonder alternatief.
Waarom dit direct impact heeft
WCAG 2.1.1 Keyboard, 2.4.3 Focus Order en 2.4.7 Focus Visible worden direct aangetast. Dit leidt tot onbruikbaarheid voor toetsenbord-only gebruikers en lagere scores in automatische validators — test met onze tool op wcagtool.nl/checker.
Zo los je dit op in code
Gebruik altijd semantische HTML als eerste keuze
Voorkeur: gebruik <button>, <a href>, <input> etc. Pas pas ARIA alleen toe als semantics ontbreken of uitbreidingen nodig zijn.
Voorbeeld: custom toggle — verkeerde aanpak
<div class="toggle" role="button" tabindex="0">Aan</div>
Correcte, testbare implementatie (HTML + JS + ARIA)
<button class="toggle" aria-pressed="false">Aan</button>
// JavaScript
document.querySelectorAll('.toggle').forEach(btn => {
btn.addEventListener('click', () => {
const pressed = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!pressed));
});
});
Waarom dit werkt
Een echte <button> ondersteunt Enter/Space en focus out-of-the-box en screenreaders begrijpen aria-pressed. Geen extra keydown handlers nodig.
Keyboard handlers als fallback (alleen wanneer semantics niet mogelijk)
// Gebruik alleen als semantische tag niet beschikbaar is
element.setAttribute('role','button');
element.setAttribute('tabindex','0');
element.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); element.click(); }
});
Focus-visible: laat gebruikers zien waar de focus is
Gebruik de moderne :focus-visible selector en een tastbare fallback voor browsers zonder ondersteuning.
/* CSS */
:focus-visible { outline: 3px solid #005fcc; outline-offset: 2px; }
/* Fallback voor oudere browsers */
.js-focus .focus-ring:focus { outline: 3px solid #005fcc; }
Focus management bij modals
Regels:
- Trap focus binnen de modal.
- Stel focus op het eerste focusbare element of op de modal zelf.
- Herstel focus naar het element dat de modal opende.
// Minimal focus trap (conceptueel)
function trapFocus(modal) {
const focusables = modal.querySelectorAll('a, button, input, textarea, select, [tabindex]:not([tabindex=\"-1\"])');
const first = focusables[0]; const last = focusables[focusables.length - 1];
function handleKey(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(); }
}
modal.addEventListener('keydown', handleKey);
return () => modal.removeEventListener('keydown', handleKey);
}
// Gebruik: const release = trapFocus(myModal); release();
Skip-links: direct naar content springen
Voeg zichtbare skip-links toe die alleen in beeld verschijnen bij focus.
<a class="skip-link" href="#maincontent">Sla navigatie over</a>
.skip-link { position: absolute; left: -999px; }
.skip-link:focus { left: 10px; top: 10px; }
Checklist voor developers
- Gebruik semantics vóór ARIA.
- Geen tabindex>0/negatief zonder documentatie. Gebruik tabindex=”0″ alleen voor elements die normaal niet focusbaar zijn en tabindex=”-1″ voor programmatic focus.
- Focus-visible altijd aanwezig (gebruik :focus-visible, geen outline: none zonder alternatief).
- Modals: trap focus, restore focus, set aria-hidden on achtergrond indien nodig.
- Custom widgets: implementeer keyboard interactions (Enter/Space/Arrow keys) volgens WAI-ARIA Authoring Practices.
- Valideer name, role, value (WCAG 4.1.2): test met accessibility tree.
- Automatiseer checks met onze checker: wcagtool.nl/checker en installeer onze plugin via wcagtool.nl/plugin.
Tips voor designers en redacties
Design tokens voor focus
Definieer een focus-token in jullie design system zodat ontwikkelaars consistente focusstijlen gebruiken.
--focus-color: #005fcc;--focus-width: 3px;
Vermijd het weghalen van focus-rings
Als ontwerpers een andere visuele indicator willen, specificeer altijd een toegankelijke variant: kleurcontrast en grootte moeten voldoen aan zichtbaarheidseisen.
Voor redacties: maak interactieve tekst-links logisch en keyboard-vriendelijk
Links moeten duidelijk tekstueel zijn; vermijd “Klik hier”. Gebruik beschrijvende linktekst en zorg dat inline widgets (zoals accordions) met Enter/Space werken.
Hoe test je dit?
1. Keyboard-only test (manueel)
- Schakel muis uit of leg de muis weg.
- Plaats focus op de pagina en navigeer via Tab/Shift+Tab; check focusvolgorde en zichtbaarheid.
- Activeer interactieve elementen met Enter/Space en controleer gedrag (toggles, dropdowns, accordions, modals).
2. Screenreader & accessibility tree
- Open NVDA (Windows) of VoiceOver (macOS) en navigeer met de rotor/keyboard. Controleer roles, names en states.
- Inspecteer via browser devtools → Accessibility pane: controleer Name/Role/Value en hidden-attributes.
3. Automatische checks + onze WCAG checker
Gebruik een mix: automatische tools vangen technische fouten en missen context. Run onze checker op wcagtool.nl/checker en installeer de plugin via wcagtool.nl/plugin voor CI en lokale feedback.
4. Concrete testcases (copy-paste)
Voer deze scenario’s uit en noteer expected outcome:
- Open een modal, Tab binnen modal en bevestig dat focus niet buiten komt. Expected: focus blijft in modal, Esc sluit modal en focus keert terug naar opener.
- Keyboard-only: navigeer naar alle interactieve controls; Expected: alle controls bereikbaar en werkend met Enter/Space.
- Custom widget: screenreader leest role en state (bijv. “aan/uit”). Expected: aria-pressed of aria-expanded is correct.
Snelle implementatie-templates
Accessible accordion (basis)
<div class="accordion">
<h3><button aria-expanded="false" aria-controls="acc1">Section 1</button></h3>
<div id="acc1" hidden>Content 1</div></div>
// JS: toggle aria-expanded and hidden
Accessible dropdown (enter/space + arrow keys)
<div class="dropdown" role="listbox" tabindex="0" aria-activedescendant="">...</div>
// Implement arrow up/down to change aria-activedescendant and Enter to select
Checklist voor release / QA
- Run keyboard-only check en documenteer uitzonderingen.
- Voer screenreader-check uit op belangrijke workflows (inloggen, check-out, formulieren).
- Automatiseer regressiechecks met onze plugin (wcagtool.nl/plugin).
- Houd een duidelijke issues-lijst met reproducerende stappen en schermopnames.
Support & tooling
Test je werk direct met onze online WCAG checker: wcagtool.nl/checker. Voor integratie en CI gebruik onze plugin: wcagtool.nl/plugin. Heb je vragen? Vul het contactformulier in — we reageren binnen 24 uur en helpen met concrete code-aanpassingen.
Praktische tip: voeg tijdens ontwikkeling een CSS-klasse toe wanneer je toetsenbordgebruik detecteert, zodat je onderscheid kunt maken tussen mouse- en keyboard-focus en ontwerpers consequente focusstijlen kunnen laten zien.
// Simple keyboard-detection to toggle .js-focus on documentElement
function enableKeyboardFocus() {
function handleFirstTab(e) {
if (e.key === 'Tab') {
document.documentElement.classList.add('js-focus');
window.removeEventListener('keydown', handleFirstTab);
}
}
window.addEventListener('keydown', handleFirstTab);
}
enableKeyboardFocus();