Keyboard en focusbeheer: praktische WCAG-implementatie
Keyboardtoegankelijkheid en correct focusbeheer gaan in de praktijk vaak fout: interactieve elementen missen focusstijlen, modals blokkeren of breken tabvolgorde, custom controls reageren niet op Enter/Space en editors vergeten focus bij fouten. Dit leidt tot gebrekkige bruikbaarheid voor toetsenbordgebruikers en faalt vaak WCAG 2.1 AA-eisen.
Wij helpen met concrete, testbare oplossingen: heldere HTML/ARIA-structuren, CSS voor zichtbare focus, JavaScript-patronen voor focus trapping en roving tabindex, plus checklists en teststappen. Test je site direct met onze WCAG checker, download onze plugin op wcagtool.nl/plugin en stel vragen via het contactformulier — we beantwoorden binnen 24 uur.
Het probleem in de praktijk
Veelvoorkomende foutbeelden
1) Geen focus-stijl of focus outline weggefilterd; 2) Custom knoppen/divs zonder keyboard handlers; 3) Modal dialogs zonder focus trap en zonder aria-hidden op achterliggende content; 4) Onlogische tabvolgorde door DOM-orde of verkeerd gebruik van tabindex; 5) Foutmeldingen die niet automatisch focus krijgen of niet programmatically focusable zijn.
Waarom dat faalt volgens WCAG
WCAG vereist keyboard-toegang (2.1.1) en duidelijke focusindicatoren (2.4.7), plus toegang tot content en bediening. Onjuiste implementatie verhindert toetsenbordgebruik en screenreader-navigatie.
Zo los je dit op in code
Basisregels
Gebruik semantische HTML waar mogelijk (<button>, <a>, <input>). Voeg tabindex alleen toe om interactieve volgorde te corrigeren (bij voorkeur tabindex=”0″ voor programmatic focus), vermijd tabindex=”1..n”. Gebruik aria-* alleen als semantiek met HTML niet mogelijk is.
Focus zichtbaar maken — CSS
.sr-focus-reset{outline:none;}.focus-visible{box-shadow:0 0 0 3px rgba(21,156,228,0.6);border-radius:4px;}button:focus, a:focus, input:focus{outline:none;}button:focus-visible, a:focus-visible, input:focus-visible{box-shadow:0 0 0 3px rgba(21,156,228,0.6);}
Gebruik :focus-visible voor moderne browsers en polyfill waar nodig. Dit voorkomt dat focus-styles verdwijnen op muisinteracties.
Skip link (eenvoudig, altijd toepassen)
<a href="#main-content" class="skip-link">Sla naar inhoud</a><style>.skip-link{position:absolute;left:-999px;top:auto;width:1px;height:1px;overflow:hidden}.skip-link:focus{position:static;left:auto;width:auto;height:auto;padding:8px;background:#fff;color:#000;z-index:9999}</style>
Toegankelijke custom controls — voorbeeld: klikbare DIV als knop
<div role="button" tabindex="0" aria-pressed="false" class="custom-btn">Lees meer</div><script>const btn=document.querySelector('.custom-btn');btn.addEventListener('click',()=>toggle());btn.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();toggle();}});function toggle(){const state=btn.getAttribute('aria-pressed')==='true';btn.setAttribute('aria-pressed',String(!state));}</script>
Modal met focus trap en aria-hidden op achtergrond
<!-- Trigger --><button id="openModal">Open modal</button><div id="page" aria-hidden="false">...rest van pagina...</div><div id="modal" role="dialog" aria-modal="true" aria-hidden="true" style="display:none"><button id="closeModal">Close</button><div tabindex="0">Eerste focus</div><button>Actie</button></div><script>const open=document.getElementById('openModal');const modal=document.getElementById('modal');const page=document.getElementById('page');const close=document.getElementById('closeModal');let focusable=null;let first=null;let last=null;open.addEventListener('click',()=>{page.setAttribute('aria-hidden','true');modal.style.display='block';modal.setAttribute('aria-hidden','false');focusable=modal.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex=\"-1\"])');first=focusable[0];last=focusable[focusable.length-1];first.focus();document.addEventListener('keydown',trap);});// trap tab functionfunction trap(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();}}close.addEventListener('click',()=>{modal.style.display='none';modal.setAttribute('aria-hidden','true');page.setAttribute('aria-hidden','false');document.removeEventListener('keydown',trap);open.focus();});</script>
Roving tabindex patroon (geselecteerde item krijgt tabindex=0)
<div role="toolbar" aria-label="format"><button tabindex="0">B</button><button tabindex="-1">I</button><button tabindex="-1">U</button></div><script>const toolbar=document.querySelector('[role=\"toolbar\"]');toolbar.addEventListener('keydown',e=>{const items=[...toolbar.querySelectorAll('button')];let i=items.indexOf(document.activeElement);if(e.key==='ArrowRight'){e.preventDefault();items[(i+1)%items.length].focus();}if(e.key==='ArrowLeft'){e.preventDefault();items[(i-1+items.length)%items.length].focus();}});toolbar.addEventListener('focusin',e=>{items=[...toolbar.querySelectorAll('button')];items.forEach(btn=>btn.setAttribute('tabindex',btn===document.activeElement?0:-1));});</script>
Checklist voor developers
- Gebruik semantische elementen (button, a, input) vóór role/ARIA
- Voeg zichtbare focus-styles toe met :focus-visible
- Behandel Enter/Space op custom controls en zorg voor keyboard handlers
- Geen tabindex>0; gebruik tabindex=”0″ of roving tabindex patroon
- Modals: aria-modal=”true”, aria-hidden op achtergrond, focus trap, restore focus
- Formulieren: focus op eerste fout en aria-invalid/aria-describedby koppelen
- Controleer screenreader-labels: use aria-label, aria-labelledby, and descriptive text
Tips voor designers en redacties
Designers — visuele focus & contrast
Zorg dat focus-indicatoren voldoen aan contrast en grootte (minimaal 2px zichtbaar surround of 3px box-shadow). Gebruik component-states (focus, hover, active) in het design system en documenteer keyboard-gedrag.
Redacties — content en interacties
Schrijf korte, betekenisvolle knopteksten; vermijd “klik hier”. Markeer interactieve elementen consistent. Plaats skip-links en zorg dat error summaries duidelijk worden gemarkeerd en programmatically focusable.
Hoe test je dit?
Handmatige tests — stappenplan
- Verlaat muis en navigeer volledig met Tab/Shift+Tab. Zijn alle interactieve items bereikbaar en in logische volgorde?
- Gebruik Enter en Space op alle knoppen en interactieve custom elementen. Werken ze identiek? (Voor links: Enter; voor buttons: Enter/Space)
- Open modals en controleer focus trap: tab moet binnen modal blijven; Escape moet sluiten (indien ontworpen).
- Inspecteer focus-indicatoren visueel en met gezette contrastmeter.
- Activeer foutscenario in formulier: focus moet springen naar foutoverzicht en ieder foutveld moet aria-describedby gebruiken.
Automated & tooling
Run onze WCAG checker voor geautomatiseerde tests en aanvullende aanbevelingen. Installeer de wcagtool plugin voor browser-integratie (axe + custom checks). Gebruik browser Accessibility tree (Chrome DevTools) om aria-hidden en tabindex te verifiëren.
Concrete testcases
Maak een testpagina met: skip link, 5 knoppen inclusief custom control, modal, formulier met 2 fouten. Test tabvolgorde, Enter/Space, screenreader-toegankelijkheid (NVDA/VoiceOver) en voer onze checker uit.
Extra concrete how-to’s
Formulierfouten automatisch focussen
function focusFirstError(){const errorEl=document.querySelector('.error, [aria-invalid=\"true\"]');if(!errorEl)return;errorEl.setAttribute('tabindex','-1');errorEl.focus();}
Roep focusFirstError() aan na server/validator response. Verwijder tijdelijke tabindex na focus indien nodig.
Prevent default op Space voor non-button elementen
element.addEventListener('keydown',e=>{if(e.key===' '){e.preventDefault();element.click();}});
Hoe wij je direct helpen
Gebruik de WCAG checker om je pagina nu te scannen. Download onze integratie op wcagtool.nl/plugin voor CI/CD checks. Zit je vast? Stuur je vraag via het contactformulier — antwoord binnen 24 uur.
Direct toepasbare code: gebruik de modal-focus-trap code hierboven als copy-paste basis en test het met onze checker.
Praktische tip: voeg in je CI een test die met puppeteer tab-strings simuleert en checkt of focus nooit buiten een open modal terechtkomt — combineer dat met onze plugin voor automatische rapportage.