Toegankelijkheidsfouten: keyboard en focusbeheer praktisch opgelost
Keyboard-toegankelijkheid en focusbeheer zijn twee van de meest voorkomende implementatiefouten bij WCAG. Ontbrekende skip-links, onzichtbare focus-stijlen, verkeerde focusvolgorde en gebrekkige focus-management bij modals of dropdowns maken een site onbruikbaar voor keyboard- en assistive-technologiegebruikers.
Wij brengen dit terug naar concrete, testbare stappen: werkende codevoorbeelden, CSS- en JS-snippets en checklists die je direct in je project kunt plakken. Test vervolgens met onze WCAG checker/validator, download onze plugin en vraag ons via het contactformulier—vragen beantwoorden we binnen 24 uur.
Het probleem in de praktijk
Veelgemaakte fouten
- Geen skip-link of onzichtbaar gemaakt door CSS
- Focus outline weggehaald zonder alternatief
- Focusverlies bij sluiten van modals of navigeren
- Non-interactieve elementen met role=”button” zonder keyboard-events
- Trap in focus bij dialogs/menus (focus trapping ontbreekt of faalt)
Voorbeeldsituaties
Forms met custom-controls die niet tabbable zijn; dropdowns die muis-only werken; focus die springt naar het einde van de pagina na AJAX-submit. Dit zijn allemaal direct reproduceerbaar en oplossen met de voorbeelden hieronder.
Zo los je dit op in code
1. Implementatie: Skip-link die altijd zichtbaar wordt bij focus
<a href="#main" class="skip-link">Sla navigatie over</a><br><!-- CSS --><br>.skip-link {position: absolute;left: -9999px;top: auto;width: 1px;height: 1px;overflow: hidden;} .skip-link:focus {position: static;left: 0;top: 0;width: auto;height: auto;padding: 8px;background: #fff;color: #000;z-index: 9999;} <br><!-- Main --><br><main id="main">...</main>
2. Gebruik toegankelijke focus-styles: behoud outline, verbeter visueel
/* Gebruik :focus-visible voor moderne browsers en fallback */<br>a:focus, button:focus, input:focus {outline: 3px solid #005fcc;outline-offset: 2px;} <br>:focus:not(:focus-visible) {outline: none;} <br>/* Zorg dat je niet volledig weglaat */
3. Rollen en keyboard events voor custom controls
<div role="button" tabindex="0" aria-pressed="false" id="customBtn">Toggle</div><br><script>const btn = document.getElementById('customBtn'); btn.addEventListener('click', toggle); btn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } }); function toggle(){ const pressed = btn.getAttribute('aria-pressed') === 'true'; btn.setAttribute('aria-pressed', String(!pressed)); } </script>
4. Focus management bij modals (open, trap, close)
<!-- Minimal modal structure --><br><button id="openModal">Open modal</button><br><div id="modal" role="dialog" aria-modal="true" aria-hidden="true"><br> <button id="closeModal">Close</button><br> <a href="#">Link in modal</a><br></div><br><script>const open = document.getElementById('openModal'); const modal = document.getElementById('modal'); const close = document.getElementById('closeModal'); let lastFocused; const focusableSelector = 'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'; open.addEventListener('click', () => { lastFocused = document.activeElement; modal.setAttribute('aria-hidden','false'); modal.style.display = 'block'; const focusable = modal.querySelectorAll(focusableSelector); focusable[0].focus(); document.body.style.overflow = 'hidden'; }); close.addEventListener('click', () => { modal.setAttribute('aria-hidden','true'); modal.style.display = 'none'; document.body.style.overflow = ''; if (lastFocused) lastFocused.focus(); }); document.addEventListener('keydown', (e) => { if (modal.getAttribute('aria-hidden') === 'false') { if (e.key === 'Tab') { const focusable = Array.from(modal.querySelectorAll(focusableSelector)); 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(); } } if (e.key === 'Escape') { close.click(); } } }); </script>
5. Schermlezers: aria-hidden correct gebruiken en inert toepassen
// Wanneer modal open is zet backdrop content inert of aria-hidden<br>document.getElementById('page').setAttribute('aria-hidden', 'true');<br>// Of gebruik inert-polyfill: element.inert = true; (verspreid in JS-builds)
Checklist voor developers
- Is alles wat interactief is tabbable of reachable via keyboard?
- Zijn focus-indicatoren zichtbaar en meetbaar (contrast, grootte)?
- Wordt focus logisch verplaatst bij overlays, dialogs en route-wijzigingen?
- Werken custom controls met Enter en Spatie én hebben ze correcte ARIA-attributes?
- Is aria-hidden niet gebruikt om elementen onbereikbaar te maken voor keyboard zonder alternatief?
- Automatische focus-traps getest met Shift+Tab en Tab en met screenreader enabled?
- Run onze WCAG checker/validator en axe-core tijdens CI (voorbeeld in testsectie).
Tips voor designers en redacties
Ontwerpregels
- Maak focusvisueel onderdeel van het design-systeem (kleur, dikte, offset)
- Vermijd hover-only interacties; ontwerp voor keyboard-first
- Gebruik duidelijke visuele hiërarchie zodat focusvolgorde voorspelbaar is
Content-richtlijnen
- Form labels altijd zichtbaar en gekoppeld met for/id of aria-labelledby
- Interne links focussen content, niet alleen visuele effecten
- Gebruik korte, duidelijke linkteksten; “Lees meer” alleen met context
Hoe test je dit?
Handmatig: keyboard-only testen
- Schakel muis uit (fysiek of negeer) en navigeer alleen met Tab, Shift+Tab, Enter, Space en pijltjestoetsen.
- Controleer of elke focusbare control zichtbare focus krijgt.
- Open en sluit modal/dialog en controleer terugzetten van focus.
- Test dropdowns en custom widgets met alleen keyboard.
Automatisch: axe-core en CI
// Voorbeeld in Node + jest + jest-puppeteer gebruikt met axe-core<br>const AxeBuilder = require('@axe-core/playwright').default; test('pagina is keyboard-accessible', async () => { await page.goto('https://jouw-site.test'); const results = await new AxeBuilder({page}).analyze(); expect(results.violations.length).toBe(0); });
Run daarnaast onze WCAG checker/validator en installeer de plugin (downloaden via /plugin) voor snel inzicht tijdens development.
Automated e2e test snippet (Cypress) voor modal focus)
cy.visit('/'); cy.get('#openModal').click(); cy.get('#modal').should('be.visible'); cy.focused().should('have.attr','id','closeModal'); cy.get('body').type('{esc}'); cy.get('#modal').should('not.be.visible'); cy.focused().should('have.id','openModal');
Concrete mini-how-to’s
Making a custom select keyboard accessible
<div role="listbox" tabindex="0" aria-activedescendant="opt1" id="listbox"><div id="opt1" role="option" aria-selected="false">Optie 1</div><div id="opt2" role="option" aria-selected="false">Optie 2</div></div><br><script>const lb = document.getElementById('listbox'); lb.addEventListener('keydown', (e) => { const active = document.getElementById(lb.getAttribute('aria-activedescendant')); if (e.key === 'ArrowDown') { e.preventDefault(); const next = active.nextElementSibling || lb.firstElementChild; active.setAttribute('aria-selected','false'); next.setAttribute('aria-selected','true'); lb.setAttribute('aria-activedescendant', next.id); next.scrollIntoView({block:'nearest'}); } if (e.key === 'Enter') { active.click(); } }); </script>
Quick CSS: zichtbare focus voor alle browsers
:focus { outline: 3px solid #0a84ff; outline-offset: 2px; } :focus:not(:focus-visible) { outline: none; } /* fallback voor browsers zonder :focus-visible */
Praktische testinstructies (kort)
- Open pagina, druk Tab tot de footer; noteer on-tabbare elementen.
- Controleer modals: open, Tab door, Shift+Tab terug, Escape sluit.
- Voer screenreader quick-run (NVDA/VoiceOver) en navigeer met virtual cursor en Tab.
- Automatiseer met onze plugin en CI: voeg de plugin toe, run checker, fix violations.
Regelmatig terugkerende fixes
Focus verdwijnt na AJAX update
Zet expliciet focus op het nieuw gegenereerde element: element.focus(); en set tabindex=-1 indien nodig voordat je focust.
Buttons zonder button-tag
Gebruik altijd <button> voor buttons. Als dat echt niet kan: role, tabindex en keyboard handlers toevoegen zoals eerder getoond, maar voorkeursvolgorde: native elements > ARIA-overrides.
Extra resources & tools
- Gebruik onze WCAG checker/validator voor snelle scans en gedetailleerde foutmeldingen.
- Download onze plugin op /plugin voor integratie in je devtools en CI.
- Vragen? Gebruik ons contactformulier—antwoord binnen 24 uur.
Praktische tip: voeg deze korte JS-check toe aan je debug-build om onbereikbare focusable elements te loggen en direct te fixen:
Array.from(document.querySelectorAll('[tabindex="-1"], [tabindex]')).forEach(el => { const t = el.getAttribute('tabindex'); if (t && parseInt(t) < 0) console.warn('Negatieve tabindex gevonden', el); });
Test je website nu met onze WCAG checker/validator, download de plugin via /plugin en stuur vragen via het contactformulier—wij reageren binnen 24 uur.