Veelgemaakte fouten bij implementatie van WCAG

WCAG praktisch: Toegankelijke interactieve componenten en focusmanagement — wcagtool.nl

Toegankelijke interactieve componenten en focusmanagement

Interactieve componenten (modals, custom selects, dropdowns, tabs, tooltips) falen in de praktijk vaak door verkeerd focusmanagement, ontbrekende keyboard-ondersteuning en misbruik of ontbreken van ARIA. Dit leidt tot onbruikbare interfaces voor toetsenbordgebruikers en screenreader-gebruikers.

Wij lossen dit op met concrete, stap-voor-stap implementaties die je direct in je project kan plakken: semantische fallback, minimale ARIA, duidelijke keyboard-logs en testcases. Test je werk meteen met onze WCAG checker/validator, download onze plugin voor CI en browser-testing, of stel vragen via ons contactformulier (antwoord binnen 24 uur).

Het probleem in de praktijk

Concrete fouten die we vaak zien:

  • Geen tabindex of verkeerd gebruik van tabindex=”0″/”-1″.
  • Vervangende controls die geen keyboard-events ondersteunen (Enter/Space/Arrow/ESC).
  • Modals zonder focustrap of zonder terugzetten van focus bij sluiten.
  • ARIA-attributen die niet overeenkomen met DOM-status (bv. aria-hidden vs display:none).
  • Geen skip-links, onlogische tabvolgorde of visuele focus styles uitgeschakeld.

Waarom dit vaak fout gaat

Ontwikkelaars bouwen visueel correcte components, maar vergeten dat interactie en status ook door schermlezers en toetsenborden moeten worden gemodelleerd. Designers leveren vaak component-states zonder keyboard-specs. Resultaat: componenten die op muis werken maar niet toegankelijk zijn.

Zo los je dit op in code

1) Basisregels — semantiek eerst

Gebruik eerst native elementen (button, select, a) en pas CSS toe. Voeg ARIA alleen toe om informatie toe te voegen die semantiek niet dekt.

<!-- Gebruik native button in plaats van div -->
<button class="btn-primary">Opslaan</button>

2) Toegankelijke button-like divs — als het echt moet

Als je een non-semantic element gebruikt (bijv. icon in SVG of span), voeg dan minimaal role en keyboard handlers toe. Gebruik aria-pressed voor toggle state.

<span role="button" tabindex="0" aria-pressed="false" id="likeBtn">♥</span>
<script>
const like = document.getElementById('likeBtn');
function toggle(e){
  if(e.type==='click' || (e.type==='keydown' && (e.key==='Enter' || e.key===' '))){
    const pressed = like.getAttribute('aria-pressed') === 'true';
    like.setAttribute('aria-pressed', String(!pressed));
    e.preventDefault();
  }
}
like.addEventListener('click', toggle);
like.addEventListener('keydown', toggle);
</script>

3) Keyboard-bediening voor custom select / dropdown

Voor dropdowns: role=”listbox”/”option” of role=”menu”. Implementeer Home/End/Arrow keys, Enter/Space om te selecteren en ESC om te sluiten.

<div class="custom-select" role="listbox" tabindex="0" aria-activedescendant="opt-1">
  <div id="opt-1" role="option" aria-selected="true">Optie 1</div>
  <div id="opt-2" role="option">Optie 2</div>
  <div id="opt-3" role="option">Optie 3</div>
</div>
<script>
const list = document.querySelector('.custom-select');
const options = Array.from(list.querySelectorAll('[role=option]'));
let index = 0;
list.addEventListener('keydown', e => {
  if(e.key === 'ArrowDown'){ index = Math.min(index+1, options.length-1); e.preventDefault(); }
  if(e.key === 'ArrowUp'){ index = Math.max(index-1, 0); e.preventDefault(); }
  if(e.key === 'Home'){ index = 0; e.preventDefault(); }
  if(e.key === 'End'){ index = options.length-1; e.preventDefault(); }
  if(['ArrowDown','ArrowUp','Home','End'].includes(e.key)){
    const id = options[index].id;
    list.setAttribute('aria-activedescendant', id);
    options.forEach((o,i)=> o.setAttribute('aria-selected', i===index));
    options[index].scrollIntoView({block: 'nearest'});
  }
  if(e.key === 'Enter' || e.key === ' '){ options[index].click(); e.preventDefault(); }
});
</script>

4) Modals: focustrap + restore focus

Belangrijk: focus naar eerste focusbaar element in modal, trap focus binnen modal, ESC sluit en focus gaat terug naar trigger.

<!-- HTML -->
<button id="openModal">Open modal</button>
<div id="modal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="dialogTitle">
  <h2 id="dialogTitle">Dialog</h2>
  <button id="closeModal">Sluiten</button>
  <input placeholder="Zoek" />
</div>

<script>
const open = document.getElementById('openModal');
const modal = document.getElementById('modal');
const close = document.getElementById('closeModal');
let lastFocus = null;

function focusable(container){
  return Array.from(container.querySelectorAll('a[href],button:not([disabled]),input,select,textarea,[tabindex]:not([tabindex="-1"])'))
    .filter(el => el.offsetWidth || el.offsetHeight || el.getClientRects().length);
}

function openModal(){
  lastFocus = document.activeElement;
  modal.setAttribute('aria-hidden','false');
  modal.style.display = 'block';
  const f = focusable(modal);
  if(f.length) f[0].focus();
  document.addEventListener('focus', trap, true);
  document.addEventListener('keydown', onKey);
}

function closeModal(){
  modal.setAttribute('aria-hidden','true');
  modal.style.display = 'none';
  document.removeEventListener('focus', trap, true);
  document.removeEventListener('keydown', onKey);
  if(lastFocus) lastFocus.focus();
}

function trap(e){
  if(!modal.contains(e.target)) e.stopPropagation(), modal.querySelectorAll('button, [tabindex]')[0].focus();
}

function onKey(e){
  if(e.key === 'Escape') closeModal();
  if(e.key === 'Tab'){
    const f = focusable(modal);
    if(f.length === 0) { e.preventDefault(); return; }
    const first = f[0], last = f[f.length-1];
    if(e.shiftKey && document.activeElement === first){ last.focus(); e.preventDefault(); }
    else if(!e.shiftKey && document.activeElement === last){ first.focus(); e.preventDefault(); }
  }
}

open.addEventListener('click', openModal);
close.addEventListener('click', closeModal);
</script>

5) Visuele focus en contrast

Verwijder nooit outlines; style ze consistent. Zorg contrast tegen achtergrond >= WCAG AA (4.5:1 voor normale tekst).

button:focus{ outline: 3px solid #005fcc; outline-offset: 2px; }
.a11y-focus{ box-shadow: 0 0 0 3px rgba(0,95,204,0.25); }

Checklist voor developers

  • Gebruik native elementen waar mogelijk.
  • Keyboard: Enter/Space activeren, Arrow/Home/End voor lists, ESC sluit dialogs.
  • Correcte ARIA: aria-expanded, aria-controls, aria-hidden, aria-activedescendant, aria-modal.
  • Tabindex rules: vermijd tabindex>0; gebruik 0 en -1 correct.
  • Visuele focus: zichtbaar, contrastrijk en consistent.
  • Verifieer schermlezer labels: aria-label/aria-labelledby/alt tekst aanwezig.
  • Restore focus bij modal sluit.
  • Test op mobiele toetsenbord-navigatie en schermlezers (NVDA/VoiceOver).

Concrete quick-fixes

Verander tabindex=”2″ etc. naar logisch DOM volgorde met tabindex=”0″ waar nodig. Vervang role=”button” op div door <button> of voeg Enter/Space handlers toe.

Tips voor designers en redacties

Designers: definieer keyboard states

Lever component-specs inclusief focus-, hover- en pressed-states en keystroke-mapping. Voorzie duidelijke visuals voor focus en geef contrastwaarden aan.

Redacties: content en ARIA

Schrijf duidelijke alt-teksten, voeg aria-describedby toe voor complexe controls en test content-omschrijvingen met screenreaders. Gebruik korte, unieke linkteksten en vermijd “klik hier”.

Hoe test je dit?

Handmatig (snelstart)

  1. Tab door de pagina: kan je alle interactieve elementen bereiken en in logische volgorde gebruiken?
  2. Gebruik Enter/Space/Arrow/ESC op custom components en noteer mislukkingen.
  3. Open modals en controleer focustrap en focus restore.
  4. Schakel afbeeldingen uit of gebruik reader-mode: alt-teksten correct?

Automatisch & tools

Gebruik onze WCAG checker/validator voor snelle scans, en installeer de wcagtool plugin in CI of browser voor regressietests. Combineer met axe-core of Pa11y voor integratie-testing.

Screenreader testing

Test met NVDA (Windows) en VoiceOver (macOS/iOS). Controleer of aria-attributes overeenkomen met visuele status. Luister naar navigatie-woorden (“button”, “link”, “expanded/collapsed”).

Meetbare testcases

  • Testcase: Modal focustrap — stappen: open modal, druk 10x Tab en Shift+Tab, ESC sluit modal en focus terug naar trigger. Verwacht: focus blijft binnen modal; ESC sluit; trigger krijgt focus terug.
  • Testcase: Custom dropdown — stappen: focus dropdown, ArrowDown, Enter. Verwacht: aria-activedescendant updaten, waarde geselecteerd.

Extra testbare codevoorbeelden

Skip-to-content link (direct toepasbaar)

<a href="#main" class="skip-link">Ga naar hoofdinhoud</a>
<style>.skip-link{position:absolute;left:-999px} .skip-link:focus{left:8px;top:8px;} </style>

ARIA live voor dynamische updates

<div aria-live="polite" id="sr-announcer" class="visually-hidden"></div>
<script>
function announce(msg){ document.getElementById('sr-announcer').textContent = msg; }
// Gebruik announce("Opslaan gelukt") na AJAX acties
</script>

Resources en acties

Test direct je pagina met onze WCAG checker. Download de plugin voor automatische scans in je workflow. Vragen? Gebruik het contactformulier — we reageren binnen 24 uur.

Wil je dat we één pagina reviewen? Upload of plak je URL in de checker en kies “grondige review”; onze experts sturen concrete code-suggesties terug.

Laatste praktische tip

Voeg deze korte helper toe om tabindex-fouten te vinden tijdens development: een script dat alle elementen met tabindex>0 markeert en console waarschuwingen geeft.

<script>
document.querySelectorAll('[tabindex]').forEach(el => {
  const t = parseInt(el.getAttribute('tabindex'),10);
  if(!Number.isNaN(t) && t > 0){
    el.style.outline = '3px dashed red';
    console.warn('tabindex>0 gevonden:', el);
  }
});
</script>

Test je implementatie nu met onze WCAG checker/validator, installeer de plugin en stel vragen via het contactformulier (antwoord binnen 24 uur).