Perceptible: hoe maak je content waarneembaar?

Praktische implementatie: Toegankelijke keyboard-navigatie en focusbeheer | WCAGtool

Toegankelijke keyboard-navigatie en focusbeheer — praktische implementatie

Keyboard-toegankelijkheid en correct focusbeheer gaan in de praktijk vaak mis: verkeerde tabindex-waardes, geen zichtbare focus-stijlen, gefocuste elementen die verdwijnen of verkeerd focusherstel na modals zijn veelvoorkomende fouten. Dat leidt tot gebrekkige navigeerbaarheid voor mensen die geen muis gebruiken en tot afkeuring bij WCAG-audits.

Wij lossen dit op met concrete, testbare patterns: semantische HTML, duidelijke tabindex-regels, CSS :focus-visible, toegankelijke modals en focus-traps, plus kant-en-klare JS-snippets om fouten direct te repareren. Test direct met onze WCAG checker (wcagtool.nl/checker), installeer onze plugin (wcagtool.nl/plugin) en neem contact op via ons formulier (wcagtool.nl/contact) — we reageren binnen 24 uur.

Het probleem in de praktijk

Veelvoorkomende fouten

  • Interactie-elementen zonder focusable attribute (div/span in plaats van button/a)
  • Onnodig gebruik van tabindex=”0″ of tabindex=”-1″ dat de tabvolgorde breekt
  • Geen of onduidelijke focus-styles; outline: none zonder alternatief
  • Modals zonder focus-trap en zonder focusherstel bij sluiten
  • Custom controls zonder ARIA/keyboard support (bijv. custom dropdowns die niet op Enter/Space reageren)

Waarom dit faalt bij tests

Automated tools detecteren vaak structurele fouten, maar het echte probleem toont zich bij manuele keyboard-tests en screenreader-sessies: verkeerde tabvolgorde, onbereikbare acties en verloren context.

Zo los je dit op in code

Gebruik semantische HTML eerst

Stap 1: vervang non-interactive elementen door native controls. Buttons en links zijn keyboard-focusable en hebben standaard assistive technologie ondersteuning.

<!-- FOUT -->
<div role="button" onclick="doAction()">Actie</div>
<!-- GOED -->
<button type="button" onclick="doAction()">Actie</button>

Zichtbare focus-styles: gebruik :focus-visible

Stap 2: geen outline: none zonder alternatief. Gebruik :focus-visible voor toegankelijke en visuele consistentie. Voor legacy browsers fallback.

/* Basis focus-style */
:focus-visible { outline: 3px solid #005fcc; outline-offset: 2px; border-radius: 4px; }

/* Fallback voor browsers zonder :focus-visible */
:focus { outline: 3px solid rgba(0,95,204,0.6); }

Tabindex-regels — houd het simpel

Stap 3: Regels:

  1. Gebruik tabindex alleen als het echt moet.
  2. tabindex=”0″ maakt element focusable in tabvolgorde — gebruik dit voor custom controls die native niet focusable zijn.
  3. tabindex=”-1″ om programmatic focus te ondersteunen (bijv. focus naar een element met JS), niet voor navigatie.

<div role="button" tabindex="0" aria-pressed="false" id="customBtn">Toggle</div>
<script>
document.getElementById('customBtn').addEventListener('keydown', function(e){
  if(e.key === ' ' || e.key === 'Enter'){ e.preventDefault(); this.click(); }
});
</script>

Toegankelijke modal: focus-trap en focusherstel

Stap-voor-stap implementatie van een eenvoudige, toegankelijke modal met focus-trap en focusherstel.

<!-- HTML -->
<button id="openModal">Open modal</button>
<div id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
  <div class="dialog">
    <h2 id="modalTitle">Modal titel</h2>
    <button id="closeModal">Close</button>
    <a href="#">Link in modal</a>
  </div>
</div>

<!-- JavaScript -->
<script>
const openBtn = document.getElementById('openModal');
const modal = document.getElementById('modal');
const closeBtn = document.getElementById('closeModal');
let lastFocused;

function trapFocus(container){
  const focusable = container.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])');
  const first = focusable[0];
  const last = focusable[focusable.length -1];
  container.addEventListener('keydown', function(e){
    if(e.key !== 'Tab') return;
    if(e.shiftKey && document.activeElement === first){ e.preventDefault(); last.focus(); }
    else if(!e.shiftKey && document.activeElement === last){ e.preventDefault(); first.focus(); }
  });
}

openBtn.addEventListener('click', () => {
  lastFocused = document.activeElement;
  modal.removeAttribute('hidden');
  document.body.style.overflow = 'hidden';
  trapFocus(modal);
  modal.querySelector('[tabindex], button, a, input, select, textarea')?.focus();
});

closeBtn.addEventListener('click', () => {
  modal.setAttribute('hidden', '');
  document.body.style.overflow = '';
  lastFocused?.focus();
});
</script>

Custom controls: ARIA + keyboard events

Voor custom widgets (dropdowns, sliders) combineer role/aria met keyboard handlers. Voorbeeld eenvoudige custom dropdown:

<div class="dropdown" role="listbox" tabindex="0" aria-activedescendant="opt1" id="dd">
  <div role="option" id="opt1" data-value="1">Optie 1</div>
  <div role="option" id="opt2" data-value="2">Optie 2</div>
</div>

<script>
const dd = document.getElementById('dd');
let current = 0;
const options = Array.from(dd.querySelectorAll('[role="option"]'));
function updateActive(i){
  options.forEach((o, idx) => {
    o.setAttribute('aria-selected', idx===i);
  });
  dd.setAttribute('aria-activedescendant', options[i].id);
  options[i].focus();
}
dd.addEventListener('keydown', e => {
  if(e.key === 'ArrowDown'){ e.preventDefault(); current = Math.min(current+1, options.length-1); updateActive(current); }
  if(e.key === 'ArrowUp'){ e.preventDefault(); current = Math.max(current-1, 0); updateActive(current); }
  if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); console.log('selected', options[current].dataset.value); }
});
</script>

Checklist voor developers

  • Gebruik semantische HTML voor interactieve elementen (buttons, links, inputs).
  • Verwijder onnodige tabindex-attributen en documenteer waar je tabindex gebruikt.
  • Zorg voor zichtbare focus-styles (gebruik :focus-visible).
  • Implementatie van modals: aria-modal, role=”dialog”, focus-trap en focusherstel.
  • Custom components: voeg correcte role/aria-* attributen toe en implementeer Enter/Space/Arrow keyboard gedrag.
  • Test met alleen toetsenbord en met screenreader (NVDA/VoiceOver).
  • Run onze WCAG checker: wcagtool.nl/checker en gebruik onze browser-plugin: wcagtool.nl/plugin.

Tips voor designers en redacties

Design tokens voor focus

Maak focus-styles onderdeel van je design system via tokens. Definieer kleur, dikte en offset zodat developers consistentie kunnen toepassen.

Content-structuur en tabvolgorde

Zorg dat redacties logisch opdelen van content: koppen, paragrafen en links in een inhoudslogische volgorde voorkomen verwarring bij tab-navigatie. Vermijd te veel interactieve elementen in één blok.

Formulierlabels en foutmeldingen

Redacties: gebruik duidelijke labels en help-tekst. Koppel foutmeldingen met aria-describedby en focus naar het foutveld na submit.

<label for="email">E-mail</label>
<input id="email" name="email" type="email" aria-describedby="emailHelp" />
<div id="emailHelp">Gebruik een geldig e-mailadres</div>

Hoe test je dit?

Manuele keyboard-test (stap-voor-stap)

  1. Schakel muis uit of leg je hand op je toetsenbord.
  2. Tab door de pagina: elk interactief element moet logisch, voorspelbaar en zichtbaar focusbaar zijn.
  3. Shift+Tab controleert reverse volgorde.
  4. Voor modals: open modal, controleer dat focus binnen modal blijft (tab) en dat sluiten focus terugzet naar opener.

Screenreader-test

  1. NVDA (Windows): start NVDA, navigeer met Tab en toets NVDA+T of gebruik de tabbladen; controleer of aria-roles/labels kloppen.
  2. VoiceOver (macOS): schakel VoiceOver in, test keyboard-navigatie en controleer dat labels en status (expanded/selected) worden voorgelezen.

Automated tools + onze checker

Run onze WCAG checker: wcagtool.nl/checker en de browser-plugin (wcagtool.nl/plugin) voor snelle resultaten. Combineer met Axe en Lighthouse. Gebruik resultaten om gerichte fixes te implementeren en opnieuw te testen.

Test-cases om direct uit te voeren

  1. Open homepage, tab door header en nav; noteer of order logisch is.
  2. Open alle modals en dialogen; controleer focus-trap en herstel.
  3. Controleer alle custom controls op Enter/Space/Arrow support en dat aria-attributes up-to-date zijn.
  4. Gebruik onze checker en stuur de rapportlink naar ons support via wcagtool.nl/contact voor hulp (antwoord binnen 24 uur).

Praktische tip: voer deze mini-scan als onderdeel van je CI/CD pipeline met onze plugin en blokkeer deployments met regressies in keyboard-toegankelijkheid. Download de plugin: wcagtool.nl/plugin en test nu: wcagtool.nl/checker.

Laatste praktische code-check (plakbaar): focus-herstel helper

function openDialog(modal, opener){
  opener = opener || document.activeElement;
  modal.removeAttribute('hidden');
  modal.setAttribute('aria-hidden', 'false');
  const focusable = modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');
  focusable?.focus();
  function closeHandler(){ modal.setAttribute('hidden',''); modal.setAttribute('aria-hidden','true'); opener?.focus(); modal.removeEventListener('dialog:close', closeHandler); }
  modal.addEventListener('dialog:close', closeHandler);
  return () => modal.dispatchEvent(new Event('dialog:close'));
}

Test je website meteen met onze WCAG checker (wcagtool.nl/checker). Heb je vragen of wil je hulp bij implementatie? Vul het contactformulier: wcagtool.nl/contact — we antwoorden binnen 24 uur.