Hoe je een WCAG-audit uitvoert: stap voor stap

WCAG Tool – Praktische implementatie: Keyboard & focus toegankelijkheid

Keyboard- en focus-toegankelijkheid: praktische implementatie

Veel sites missen bruikbare keyboardnavigatie, consistente focusstijlen en correcte ARIA-structuur. Resultaat: gebruikers met toetsenbord, screenreaders of beperkte motoriek stuiten op onbruikbare widgets, modals die focus ‘vangen’ of links zonder duidelijke focusindicator.

Wij lossen dit op met concrete, stap-voor-stap codevoorbeelden en testbare checks die je meteen in je project kunt plakken. Test je resultaat altijd met onze WCAG checker/validator, download onze plugin en neem bij vragen contact op via het contactformulier (antwoord binnen 24 uur).

Het probleem in de praktijk

Typische fouten die we dagelijks tegenkomen:

  • Interactieve elementen zijn geen echte buttons/links (div/span zonder role/keyboard handlers).
  • Custom controls missen ARIA-roles, states en keyboard support.
  • Modals en dropdowns breken focusmanagement: focusverdwijnt of gebruikers blijven ‘gevangen’.
  • Visuele focusindicatoren zijn verwijderd door CSS of onzichtbaar op bepaalde achtergronden.
  • Formulieren zonder labels of met placeholder-only labels.

Waarom dit faalt in teams

Sprints en UI-acceptatie letten op visueel resultaat, niet op keyboardflow. Designers leveren component-exports zonder keyboard-cases. Content-editors plaatsen links en buttons zonder context. Wij adviseren het opbreken van issues in concrete implementatiestappen en checklists.

Zo los je dit op in code

1) Gebruik semantische elementen

Vervang divs met onClick door echte elementen. Dit is altijd de eerste stap.

<!-- fout -->
<div class="btn" onclick="doAction()">Opslaan</div>

<!-- goed -->
<button type="button" class="btn" onclick="doAction()">Opslaan</button>

2) Focusstijl: prefereer :focus-visible

Zorg dat focus zichtbaar is zonder visuele rommel. Gebruik deze CSS:

/* toon focus alleen bij keyboard */
:focus { outline: none; }
:focus-visible { outline: 3px solid #005fcc; outline-offset: 2px; border-radius: 3px; }

Als je ondersteuning voor oudere browsers nodig hebt, gebruik de polyfill van focus-visible of fallback stijl voor :focus.

3) Toegankelijke custom buttons / links

Als je een niet-semantic element moet gebruiken (bv. custom SVG), voeg role, tabindex en keyboard handlers toe:

<span role="button" tabindex="0" aria-pressed="false" id="toggleSave">
  <svg ...>...</svg>
</span>

<script>
const btn = document.getElementById('toggleSave');
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) Dropdowns en menu’s: support arrow-keys en aria

Gebruik rolen en manage focus expliciet. Kort voorbeeld voor een simple menu:

<div role="menu" aria-label="Opties" id="menu">
  <button role="menuitem" tabindex="-1">Profiel</button>
  <button role="menuitem" tabindex="-1">Instellingen</button>
  <button role="menuitem" tabindex="-1">Uitloggen</button>
</div>

<script>
const menu = document.getElementById('menu');
const items = Array.from(menu.querySelectorAll('[role="menuitem"]'));
let idx = 0;
function openMenu(){ items.forEach((i, n)=> i.tabIndex = n===0?0:-1); items[0].focus(); }
menu.addEventListener('keydown', e => {
  if (e.key === 'ArrowDown'){ e.preventDefault(); idx = (idx+1)%items.length; items[idx].focus(); }
  if (e.key === 'ArrowUp'){ e.preventDefault(); idx = (idx-1+items.length)%items.length; items[idx].focus(); }
  if (e.key === 'Home'){ e.preventDefault(); idx = 0; items[idx].focus(); }
  if (e.key === 'End'){ e.preventDefault(); idx = items.length-1; items[idx].focus(); }
});
</script>

5) Modals: focus trap en restore

Belangrijkste regels: trap focus binnen modal, sluit modal op ESC, restore focus naar opener.

<!-- HTML -->
<button id="openModal">Open dialog</button>
<div id="modal" role="dialog" aria-modal="true" aria-hidden="true">
  <div role="document">
    <button id="closeModal">Sluiten</button>
    <input>
  </div>
</div>

<script>
const opener = document.getElementById('openModal');
const modal = document.getElementById('modal');
const close = document.getElementById('closeModal');
let lastFocused;
function open(){ lastFocused = document.activeElement; modal.style.display='block'; modal.removeAttribute('aria-hidden'); trapFocus(modal); }
function closeModal(){ modal.style.display='none'; modal.setAttribute('aria-hidden','true'); lastFocused?.focus(); }
opener.addEventListener('click', open);
close.addEventListener('click', closeModal);
document.addEventListener('keydown', e => { if (e.key === 'Escape' && modal.style.display==='block'){ closeModal(); } });

function trapFocus(container){
  const focusable = Array.from(container.querySelectorAll('a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])')).filter(n => n.offsetParent !== null);
  let idx = 0; focusable.forEach((el,i)=>el.addEventListener('keydown', e => {
    if (e.key === 'Tab'){ if (e.shiftKey){ if (i===0){ e.preventDefault(); focusable[focusable.length-1].focus(); } } else { if (i===focusable.length-1){ e.preventDefault(); focusable[0].focus(); } } }
  }));
  focusable[0]?.focus();
}
</script>

6) Formulieren: altijd labels en aria-invalid

Voorzie expliciete labels, foutboodschappen met aria-describedby en aria-invalid:

<label for="email">E-mail</label>
<input id="email" name="email" type="email" aria-describedby="emailHelp" />
<div id="emailHelp">We sturen nooit spam</div>
<!-- bij validatie -->
<input id="email" aria-invalid="true" aria-describedby="emailError" />
<div id="emailError">Geen geldig e-mailadres</div>

Checklist voor developers

  • Gebruik semantische HTML (button, a, input) ipv div/span voor interactie.
  • Voeg zichtbare focusstijlen toe (:focus-visible).
  • Voeg keyboard handlers toe voor custom controls (Enter/Space/Arrow/ESC).
  • Gebruik correcte ARIA-roles, states en properties; geen ARIA als native element volstaat.
  • Trap focus in modals en restore focus bij sluiten.
  • Test met alleen toetsenbord binnen 5 minuten: alle functionaliteit moet bereikbaar zijn.
  • Automatiseer checks in CI met onze WCAG checker of axe-core/pa11y.

Mini-how-to: CI-check met axe-core

npm install --save-dev axe-core puppeteer
node scripts/run-axe.js

// scripts/run-axe.js (vereenvoudigd)
const puppeteer = require('puppeteer');
const {injectAxe, checkA11y} = require('axe-puppeteer');
(async () => {
 const browser = await puppeteer.launch();
 const page = await browser.newPage();
 await page.goto('https://mijnsite.test');
 await injectAxe(page);
 const results = await checkA11y(page);
 console.log(JSON.stringify(results, null, 2));
 await browser.close();
})();

Of gebruik direct onze online validator en de browser-plugin om snel een rapport te krijgen.

Tips voor designers en redacties

Designers: houd rekening met focus en states

  • Documenteer focusstijlen in alle componenten (focus, active, disabled).
  • Stel keyboard-windows/flows op voor modals, menu’s en dialogs.
  • Lever component bibliotheken met aria-varianten en voorbeelden van keyboard-interactie.

Redacties: linktekst, alt en context

  • Gebruik betekenisvolle linkteksten: “Lees meer over privacy” ipv “Lees meer”.
  • Alt-tekst beschrijft functie: voor decoratieve afbeeldingen alt=””.
  • Gebruik headings hiërarchie H1-H6 semantisch — dit helpt screenreader-navigatie.

Wil je designchecklists als PDF? Download onze resources op de pluginpagina of stuur een bericht via het contactformulier (antwoorden binnen 24 uur).

Hoe test je dit?

Handmatig: concrete keyboard-teststappen

  1. Schakel muis uit of leg handen op toetsenbord.
  2. Gebruik Tab/Shift+Tab om door interface te navigeren — elke interactieve controle moet focus tonen en actie uitvoeren op Enter/Space.
  3. Bekijk modals: open modal, test Tab-cycling, toets Escape om te sluiten, controleer dat focus terugkeert naar opener.
  4. Controleer dropdowns/menu’s met ArrowUp/ArrowDown, Home/End.
  5. Valideer formulieren zonder muis: labels, foutmeldingen voor schermlezers via aria-describedby.

Automatisch: tooling en commando’s

Aanbevolen tools: axe, pa11y, Lighthouse en onze WCAG checker. Voor snelle CLI:

npx pa11y https://mijnsite.test
npx lighthouse https://mijnsite.test --only-categories=accessibility --output=json

Testscript: puppeteer keyboard sanity

// puppeteer keyboard-sanity.js (vereenvoudigd)
const puppeteer = require('puppeteer');
(async () => {
 const b = await puppeteer.launch();
 const p = await b.newPage();
 await p.goto('https://mijnsite.test');
 await p.keyboard.press('Tab');
 await p.screenshot({path:'tab1.png'});
 await p.keyboard.press('Tab');
 await p.screenshot({path:'tab2.png'});
 await b.close();
})();

Dergelijke screenshots in CI helpen regressies detecteren. Vergeet niet onze online validator te draaien voor gedetailleerde WCAG-rapporten.

Extra testcases die je direct kunt draaien

  • Tab-only: alle primaire acties bereikbaar binnen 7 Tab-stappen op cruciale pagina’s.
  • Screenreader-quickcheck: navigeer headings (h), links (1) en landmarks (r) — moet logisch zijn.
  • Contrast-scan: alle tekst moet voldoen aan AA/AAA volgens de validator.

Gebruik onze WCAG checker voor een snelle scan en download de plugin om tijdens development live feedback te krijgen.

Laatste praktische tip

Plak deze korte functie in je project voor een directe focus-reset op belangrijke navigatie-acties — werkt als snelle patch totdat je componenten hebt geüpdatet:

// focus-reset.js
export function resetFocusTo(selector){
  const el = document.querySelector(selector);
  if (!el) return;
  el.setAttribute('tabindex','-1');
  el.focus({preventScroll:true});
  // verwijder tijdelijk tabindex zodat het element weer normaal navigeerbaar is
  setTimeout(()=> el.removeAttribute('tabindex'), 100);
}

Test je site nu met onze WCAG checker/validator, installeer de plugin en stuur vragen via het contactformulier — we reageren binnen 24 uur.