Keyboard- en focus-toegankelijkheid voor interactieve componenten
In de praktijk gaan keyboardbediening en focusbeheer vaak fout omdat teams bouwen met visuele aannames: elementen zijn klikbaar met de muis maar missen semantiek, tabindex of focus-styling. Dat leidt tot onbruikbare widgets voor keyboard-only gebruikers en screenreader-bezoekers.
Wij helpen pragmatisch: concrete patterns, kant-en-klare code-snippets en testbare stappen waarmee developers, designers en redacteurs direct aan de slag kunnen. Test je site met onze WCAG checker/validator, download onze plugin en vraag ons iets via het contactformulier — we reageren binnen 24 uur.
Het probleem in de praktijk
Veelvoorkomende fouten die we tegenkomen:
- Clickable divs zonder role of tabindex, niet bereikbaar met toetsenbord.
- Custom widgets (dropdowns, modals, tabs) zonder ARIA-rollen of focus-trap.
- Geen consistente focusring of focus zichtbaar gemaakt met CSS.
- Focusverlies na sluiten van een dialog of navigatie naar dynamische content.
- Gebruik van aria-hidden zonder de DOM-visitability te regelen waardoor screenreaders content negeren.
Zo los je dit op in code
Algemene regels (kort)
- Gebruik native controls waar mogelijk (<button>, <select>, <a href>).
- Als je custom controls maakt: geef rol, tabindex, aria-expanded/aria-controls, en manage keyboard events.
- Zorg voor zichtbare focus (zie CSS voorbeeld).
- Trap focus in modals en restore focus bij sluiten.
Voorbeeld 1: toegankelijke klikbare kaart (card) – HTML + CSS
<article class="card" role="button" tabindex="0" aria-pressed="false"><h2>Productnaam</h2><p>Korte omschrijving</p></article>/* CSS */.card{border:1px solid #ddd;padding:12px;border-radius:6px;} .card:focus{outline:3px solid #005fcc;outline-offset:3px;} .card[role="button"]{cursor:pointer;}
JS: keyboard activation en ARIA state
document.querySelectorAll('.card[role="button"]').forEach(card=>{card.addEventListener('click',()=>toggle(card));card.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();toggle(card);}});function toggle(el){const pressed = el.getAttribute('aria-pressed')==='true';el.setAttribute('aria-pressed',String(!pressed));}}
Voorbeeld 2: toegankelijke custom dropdown
<div class="dropdown" id="dd1"><button id="dd1-toggle" aria-haspopup="listbox" aria-expanded="false">Keuze</button><ul id="dd1-list" role="listbox" tabindex="-1" aria-labelledby="dd1-toggle"><li role="option" tabindex="0" aria-selected="false">Optie A</li><li role="option" tabindex="-1" aria-selected="false">Optie B</li></ul></div>
/* JS: open/close, arrow navigation */const toggle = document.getElementById('dd1-toggle');const list = document.getElementById('dd1-list');let open=false;toggle.addEventListener('click',()=>{open=!open;toggle.setAttribute('aria-expanded',String(open));list.style.display=open?'block':'none';if(open){focusOption(list.querySelector('[role=option]'));} else {toggle.focus();}});list.addEventListener('keydown',e=>{const items=[...list.querySelectorAll('[role=option]')];const idx=items.indexOf(document.activeElement);if(e.key==='ArrowDown'){e.preventDefault();const next=items[Math.min(items.length-1,idx+1)];next.focus();}if(e.key==='ArrowUp'){e.preventDefault();const prev=items[Math.max(0,idx-1)];prev.focus();}if(e.key==='Enter'||e.key===' '){e.preventDefault();select(items[idx]);}});function focusOption(opt){opt.setAttribute('tabindex','0');opt.focus();}function select(opt){items=[...list.querySelectorAll('[role=option]')];items.forEach(i=>i.setAttribute('aria-selected','false'));opt.setAttribute('aria-selected','true');toggle.textContent=opt.textContent;toggle.setAttribute('aria-expanded','false');list.style.display='none';toggle.focus();}
Voorbeeld 3: modal dialog met focus-trap en restore
<button id="openModal">Open modal</button><div id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" style="display:none"><h2 id="modalTitle">Titel</h2><button id="closeModal">Sluit</button></div>
/* JS */const open=document.getElementById('openModal');const modal=document.getElementById('modal');const close=document.getElementById('closeModal');let lastFocus=null;open.addEventListener('click',()=>{lastFocus=document.activeElement;modal.style.display='block';const focusables = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');focusables[0].focus();document.addEventListener('focus',trap,true);});close.addEventListener('click',()=>{modal.style.display='none';document.removeEventListener('focus',trap,true);if(lastFocus) lastFocus.focus();});function trap(e){if(!modal.contains(e.target)){e.stopPropagation();modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])').focus();}}
Focus-styling die werkt met :focus-visible
/* CSS */:focus{outline:none;} :focus-visible{outline:3px solid #005fcc;outline-offset:2px;border-radius:4px;} /* fallback voor browsers zonder focus-visible */
Checklist voor developers
- Gebruik native controls waar mogelijk (vooral forms en links).
- Geef interactieve elementen semantische rollen (role=”button”, role=”listbox”, role=”dialog”).
- Zet tabindex=”0″ voor focusable non-native elementen en tabindex=”-1″ voor programmatically focusable elementen.
- Voeg keyboard-activatie toe (Enter, Space) en behandel Arrow keys waar relevant.
- Gebruik aria-expanded, aria-controls, aria-selected, aria-hidden correct en houd states synchroon met DOM.
- Trap focus in modals en restore focus naar de opener bij sluiten.
- Zorg voor zichtbare focus (focus-visible) en voorkom ‘outline: none’ zonder alternatief.
- Test met screenreaders (NVDA/VoiceOver) en keyboard-only navigatie.
Tips voor designers en redacties
Designers: focus first
Ontwerp altijd de focus-staat samen met visuele states (hover, active). Geef voldoende contrast aan de focusring en test in high-contrast modi. Vermijd kleine focus targets <24x24px; vergroot clickable gebieden met padding.
Redacties: semantiek en content structure
Gebruik correcte heading-hiërarchie (<h1>-<h6>), link descriptive anchor text en vermijd “Klik hier”. Bij dynamische updates: voeg aria-live regions of duidelijke link-targets zodat keyboard- en screenreadergebruikers weten dat content veranderde.
Praktische handeling voor teams
Maak component-standaarden in je design system: kopieerbare code-snippets, ARIA-voorschriften en keyboard-gedrag per component (button, dropdown, modal, tablist). Voeg unit-tests toe voor focus en keyboard via end-to-end tests (Cypress + axe of Playwright).
Hoe test je dit?
Handmatige tests (snel en effectief)
- Keyboard-only: tab door de pagina. Alle interactieve items moeten in logische volgorde en zichtbaar focusen.
- Activeer elementen met Enter en Space, navigeer dropdowns met Arrow keys.
- Open modal: focus moet in de modal blijven; sluiten moet focus terugzetten naar opener.
- Gebruik screenreader: NVDA (Windows) of VoiceOver (macOS). Controleer labels en aria-states.
- Controleer aria-hidden: verborgen content mag niet fokusbaar zijn en moet genegeerd worden door screenreaders.
Automated checks
Gebruik onze WCAG checker/validator voor snelle audits en onze browser-plugin voor inline feedback. Combineer met axe-core of pa11y in CI voor regressie-guards.
E2E tests (aanpak)
Schrijf tests die keyboard-navigatie simuleren en focus-restoratie controleren. Voorbeeld (Puppeteer/Playwright concept): controleer dat na openen van een modal de document.activeElement binnen modal zit en na sluiten terugkeert naar opener.
Testcase voorbeelden
// Playwright pseudo-code await page.click('#openModal'); const activeInModal = await page.evaluate(()=>document.activeElement.closest('#modal')!==null); expect(activeInModal).toBeTruthy(); await page.click('#closeModal'); const activeIsOpener = await page.evaluate(()=>document.activeElement.id==='openModal'); expect(activeIsOpener).toBeTruthy();
Concrete mini-how-to’s
1) Snel template voor toegankelijke button
<button class="primary">Koop</button>/* altijd voorkeur: native button */
2) Als je een div als knop gebruikt
<div role="button" tabindex="0" aria-pressed="false">Klik</div>/* voeg keydown handler voor Enter/Space toe in JS */
3) Verberg visueel maar behoud voor screenreader
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
Praktische checklist om vandaag te implementeren
- Voeg focus-visible CSS toe project-breed.
- Audit custom interactive components: voeg role, tabindex en keyboard handlers toe.
- Implementeer focus-trap in modals en restore focus.
- Voer keyboard-only en screenreader-test op 10 kritieke pagina’s.
- Run onze WCAG checker/validator en installeer de plugin in je browser; fix de top 10 issues die de tool rapporteert.
Calls-to-action en support
Gebruik onze WCAG checker/validator om direct problemen te vinden: WCAG checker/validator. Download onze plugin voor inline feedback: Download plugin. Vragen? Gebruik ons contactformulier — antwoord binnen 24 uur.
Laatste praktische tip
Voeg deze kleine JS-functie toe als quick-safety voor focusverlies in dynamische updates (plak in je global helpers):
function safeFocus(el){try{el.focus();if(document.activeElement!==el){el.setAttribute('tabindex','-1');el.focus();}}catch(e){/* fallback */}}
Test je website direct met onze WCAG checker/validator en installeer de plugin voor realtime feedback. Nog vragen? Vul het contactformulier in — we antwoorden binnen 24 uur.