Welke organisaties zijn verplicht om aan WCAG te voldoen?

Keyboard-toegankelijkheid en focusbeheer — Praktische implementatie | WCAGTool

Keyboard-toegankelijkheid & focusbeheer: praktisch toepassen

In de praktijk faalt toegankelijkheid vaak op keyboard-niveau: interactieve componenten zijn niet focusable, focusstijlen worden weggepoetst, of modals en custom controls breken de tabbeloop. Dat leidt tot onbereikbare interfaces voor toetsenbordgebruikers en screenreadergebruikers.

Wij vertalen WCAG-vereisten naar direct inzetbare oplossingen: compacte code-snippets, patterns voor modals, dropdowns en formulieren, plus testbare checklists. Test je site meteen met onze WCAG checker of installeer onze plugin voor geautomatiseerde detectie — en bij vragen helpen we binnen 24 uur via ons contactformulier.

Het probleem in de praktijk

Veelvoorkomende fouten

  • Interactie-elementen zonder native semantics (div/button misbruik) of zonder tabindex/role.
  • Focus indicatoren verwijderd via outline: none; waardoor gebruikers niet kunnen zien waar de focus is.
  • Modals en dialogs zonder focus trap of zonder terugzetten van focus bij sluiten.
  • Custom controls (dropdowns, tabs) zonder ARIA-attributes en keyboard-handlers.
  • Skip-links of header structuren ontbreken, content-editors plaatsen content zonder logische heading-hiërarchie.

Waarom dat problemen oplevert

WCAG vereist dat functionaliteit toegankelijk is via toetsenbord (2.1.1) en dat focus visueel zichtbaar is (2.4.7 / 2.4.11 afhankelijk van context). Deze fouten leiden tot niet-conforme pagina’s en slechte gebruikerservaring.

Zo los je dit op in code

Basisprincipe: gebruik native controls altijd waar mogelijk

Prefer <button>, <a> met href en form-elementen boven generieke <div> of <span>. Native elementen brengen automatisch keyboard- en focusgedrag mee.

Skip-link: zet dit direct bovenaan je page

<a href="#maincontent" class="skip-link">Direct naar hoofdinhoud</a>
<!-- CSS -->
.skip-link{position:absolute;left:-999px;top:auto;width:1px;height:1px;overflow:hidden}
.skip-link:focus{position:static;left:0;top:0;width:auto;height:auto;padding:8px;background:#005a9c;color:#fff;z-index:9999}

Focusstijlen: maak ze zichtbaar en stijlbaar

/* Goede focusstijl, verwijder nooit alle outlines */
:focus{outline:3px solid #ffbf47;outline-offset:2px;border-radius:2px}
/* Of op specifieke elementen */
button:focus,a:focus,input:focus,select:focus,textarea:focus{box-shadow:0 0 0 3px rgba(0,90,156,0.18)}

Maak custom controls keyboard-navigable (dropdown voorbeeld)

Voorbeeld: een eenvoudige custom dropdown met ARIA en keyboard-handling.

<div class="dropdown" id="dd1">
  <button id="dd1-toggle" aria-haspopup="listbox" aria-expanded="false">Kies optie</button>
  <ul id="dd1-list" role="listbox" tabindex="-1" hidden>
    <li role="option" data-value="1" tabindex="0">Optie 1</li>
    <li role="option" data-value="2" tabindex="-1">Optie 2</li>
    <li role="option" data-value="3" tabindex="-1">Optie 3</li>
  </ul>
</div>
<script>
const toggle = document.getElementById('dd1-toggle');
const list = document.getElementById('dd1-list');
let options = Array.from(list.querySelectorAll('[role=\"option\"]'));
let focusedIndex = -1;
toggle.addEventListener('click', ()=>{const expanded = toggle.getAttribute('aria-expanded')==='true';toggle.setAttribute('aria-expanded', String(!expanded));list.hidden = expanded; if(!expanded){list.focus(); focusedIndex=0;options[0].tabIndex=0;options[0].focus();}});
toggle.addEventListener('keydown', e=>{if(e.key==='ArrowDown'){e.preventDefault();toggle.click();}});
list.addEventListener('keydown', e=>{
  if(e.key==='ArrowDown'){e.preventDefault(); focusedIndex = Math.min(focusedIndex+1, options.length-1); options.forEach(o=>o.tabIndex=-1); options[focusedIndex].tabIndex=0; options[focusedIndex].focus();}
  if(e.key==='ArrowUp'){e.preventDefault(); focusedIndex = Math.max(focusedIndex-1, 0); options.forEach(o=>o.tabIndex=-1); options[focusedIndex].tabIndex=0; options[focusedIndex].focus();}
  if(e.key==='Enter' || e.key===' '){e.preventDefault(); selectOption(options[focusedIndex]);}
  if(e.key==='Escape'){toggle.click(); toggle.focus();}
});
options.forEach((opt,i)=>{opt.addEventListener('click', ()=> selectOption(opt)); opt.addEventListener('focus', ()=>{focusedIndex=i;});});
function selectOption(opt){toggle.textContent = opt.textContent; toggle.setAttribute('data-value', opt.dataset.value); toggle.setAttribute('aria-expanded','false'); list.hidden = true; toggle.focus();}
</script>

Focus trap voor modals: minimale implementatie

Zorg dat focus binnen de modal blijft en dat focus teruggezet wordt naar de trigger na sluiten.

<button id="openModal">Open dialog</button>
<dialog id="modal">
  <form method="dialog">
    <p>Inhoud</p>
    <button id="closeModal">Sluit</button>
  </form>
</dialog>
<script>
const open = document.getElementById('openModal');
const dialog = document.getElementById('modal');
const close = document.getElementById('closeModal');
open.addEventListener('click', ()=>{dialog.showModal(); const focusable = dialog.querySelectorAll('a,button,input,select,textarea,[tabindex]:not([tabindex=\"-1\"])'); focusable[0].focus(); trapFocus(dialog);});
close.addEventListener('click', ()=>{dialog.close(); open.focus();});
function trapFocus(modal){
  modal.addEventListener('keydown', function(e){
    if(e.key !== 'Tab') return;
    const focusable = Array.from(modal.querySelectorAll('a,button,input,select,textarea,[tabindex]:not([tabindex=\"-1\"])')).filter(el => !el.hasAttribute('disabled'));
    if(focusable.length===0) return;
    const first = focusable[0], 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(); }
  });
}
</script>

Formulieren: labels en duidelijke focus-volgorde

<label for="email">E-mail</label>
<input id="email" name="email" type="email" required aria-required="true">
<!-- Gebruik aria-describedby voor aanvullende instructie -->
<small id="emailHelp">We sturen alleen belangrijke berichten.</small>
<input aria-describedby="emailHelp">

Praktische ARIA-richtlijn: alleen als enhacement

Gebruik ARIA om semantics aan te vullen, nooit als vervanging van native elements. Bijvoorbeeld: gebruik role=”button” alleen als je geen <button> kunt gebruiken, en implementeer toetsenbordgedrag en aria-pressed/aria-expanded consistent.

Checklist voor developers

  • Gebruik native controls tenzij strikt onvermijdelijk.
  • Voeg zichtbare focusstijlen toe; verwijder outline: none niet zonder alternatief.
  • Implementeer skip-links en logische heading-structuur (H1..H6).
  • Zorg dat modals traps en return-focus hebben.
  • Custom widgets: voeg role, aria-attributes, tabindex & keyboard handlers toe.
  • Voer keyboard-only navigatie en screenreader-tests uit (zie testinstructies).
  • Automatiseer checks met onze WCAG checker/validator en installeer de plugin voor CI-checks.

Tips voor designers en redacties

Designers — focus en zichtbaarheid

  • Ontwerp voldoende contrast voor focus-indicatoren (3:1 tegen achtergrond als visuele aanwijzing).
  • Maak component states (focus/hover/active) expliciet in de designbibliotheek.
  • Documenteer keyboard-flows in PRD/component specs.

Redacties — contentstructuur en headings

  • Gebruik logische heading-hiërarchie; elke pagina één H1.
  • Voeg contextuele aria-describedby alleen toe als de extra tekst voor alle gebruikers relevant is.
  • Vermijd inline styles die focusstijlen overschrijven; controleer CMS-templates op tabindex-uitroepen.

Hoe test je dit?

1. Handmatig: keyboard-only

  1. Schakel muis uit of leg je handen op je toetsenbord.
  2. Tab door de pagina: kan je alle interactieve items bereiken in logische volgorde?
  3. Gebruik Shift+Tab om terug te gaan en controleer focus volgorde.
  4. Open en sluit modals met Enter/Escape, navigeer in custom widgets met pijltjestoetsen.

2. Met screenreader

  • Windows: NVDA (Insert+T voor titel check, Tab en Arrow voor navigatie).
  • Mac: VoiceOver (Cmd+F5) — oefen navigatie tussen headings en links (VO+H en VO+K).

3. Browser tools en automated checks

  • Use browser devtools Accessibility pane to inspect role/label/focusable properties.
  • Lighthouse en axe-core integraties voor developer feedback.
  • Run onze WCAG checker/validator op je pagina voor directe rapportage van keyboard- en ARIA-issues — test nu op https://wcagtool.nl of installeer onze browserplugin voor realtime feedback.

4. CI / E2E tests

Automatiseer keyboard-routes in Cypress/Playwright. Voorbeeld Playwright snippet die checkt dat modals focus terugzetten:

// Playwright (Node)
await page.click('#openModal');
await expect(page.locator('dialog')).toBeVisible();
await page.keyboard.press('Escape');
await expect(page.locator('#openModal')).toBeFocused();

Extra testcases die je direct kunt draaien

  • Tab tot footer zonder focus-tegenvallers (assuming no trapped focus).
  • Open navigation, navigeer met pijltjestoetsen in menulijsten.
  • Form validatie: focus gaat naar eerste foutief veld met aria-invalid/aria-describedby.

Calls-to-action en ondersteuning

Test je pagina nu met onze gratis WCAG checker/validator op https://wcagtool.nl/checker. Wil je directe integratie? Download onze plugin op https://wcagtool.nl/plugin en voeg CI-checks toe. Vragen? Gebruik ons contactformulier op https://wcagtool.nl/contact — we beantwoorden binnen 24 uur.

Laatste praktische tip

Voeg deze globale helper toe aan je project om debugging van focus-issues tijdens ontwikkeling snel te detecteren:

// Development helper: force visible focus outlines
if(location.search.includes('show-focus')){const style=document.createElement('style');style.textContent=':focus{outline:3px solid rgba(255,191,71,0.95)!important;outline-offset:2px !important}';document.head.appendChild(style);console.info('Focus visuals forced');}

Test direct met onze tool: https://wcagtool.nl/checker — of installeer de plugin en stuur je vraag via het contactformulier; wij reageren binnen 24 uur.