Automatische testen versus handmatige audits

Keyboard en focusmanagement: praktische WCAG-implementatie | WCAGTool

Keyboard & focusmanagement: praktische WCAG-implementatie

Keyboard-navigatie en correct focusmanagement zijn in praktijk vaak de zwakste schakel: interactieve componenten die niet via Tab bereikbaar zijn, modals zonder focustrap, of visuele focus die ontbreekt. Dat leidt direct tot onbruikbaarheid voor toetsenbordgebruikers en scoren slecht op WCAG 2.1/2.2.

Bij WCAGTool vertalen we richtlijnen naar herbruikbare, testbare patterns: concrete HTML/CSS/JS-snippets, checklists en teststappen zodat developers, frontend engineers, designers en redacties direct kunnen implementeren en valideren. Test je site meteen met onze WCAG checker, download onze plugin en neem bij vragen contact op via het contactformulier — we reageren binnen 24 uur.

Het probleem in de praktijk

Veel fouten die we tegenkomen:

  • Interactie met mouse-only listeners (onclick) zonder keyboard-equivalent (Enter/Space).
  • Verkeerd of overmatig gebruik van tabindex=”0″/”-1″, waardoor focusvolgorde breekt.
  • Modals en dialogs zonder focustrap en zonder terugzetten van focus bij sluiten.
  • Custom controls (tabs, radio-achtige knoppen, dropdowns) zonder ARIA-rollen of zonder roving tabindex.
  • Geen zichtbare focusstijl of focus wordt weggehaald met outline: none.

Deze fouten veroorzaken directe problemen voor toetsenbordgebruikers en schaden WCAG-criteria zoals 2.1.1 Keyboard en 2.4 Focus zichtbaar. Hieronder praktische oplossingen en testbare code.

Zo los je dit op in code

Basisprincipes

  • Altijd een keyboard-gebeurtenis voor interactieve controls: respond op Enter en Space.
  • Gebruik semantische elementen waar mogelijk (<button>, <a href>, <input>).
  • Laat focus zichtbaar en onderscheidbaar: gebruik :focus en prefer-reduced-motion respecteren.

Skip-link (onmisbaar)

<a href="#main" class="skip-link">Skip to main content</a>
<style>
.skip-link{position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden}
.skip-link:focus{position:static;left:auto;top:auto;width:auto;height:auto;padding:8px 12px;background:#000;color:#fff;z-index:9999}
</style>

Focus-styles: voorbeeld

/* behoud goede focus zichtbaarheid */
:focus{outline:3px solid #ffbf47;outline-offset:3px}
button:focus, a:focus{box-shadow:0 0 0 3px rgba(0,123,255,0.25)}
/* respecteer reduced motion */
@media (prefers-reduced-motion: reduce){*{scroll-behavior:auto}}

Correct gebruik van tabindex

/* semantische controls eerst, alleen voor custom focusable items: */
<div role="button" tabindex="0" aria-pressed="false">Toggle</div>
/* Gebruik tabindex="-1" om programmatic focus mogelijk te maken zonder tabstops */
button.open-modal{ /* opent modal */ }
modalElement.focus(); /* verplaats focus programmatically met tabindex="-1" op het doel */

Modal / Dialog: focus trap en terugzetten

<!-- HTML -->
<button id="openModal">Open dialog</button>
<div id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
  <h2 id="modalTitle">Dialog</h2>
  <button id="closeModal">Close</button>
</div>

/* JS - eenvoudig focustrap */
const openBtn = document.getElementById('openModal');
const modal = document.getElementById('modal');
const closeBtn = document.getElementById('closeModal');
let previouslyFocused = null;
openBtn.addEventListener('click', ()=>{ previouslyFocused = document.activeElement; modal.hidden = false; modal.setAttribute('tabindex','-1'); modal.focus(); document.addEventListener('focus', enforceTrap, true);});
closeBtn.addEventListener('click', closeModal);
function enforceTrap(e){ if(!modal.contains(e.target)){ e.stopPropagation(); modal.focus(); }}
function closeModal(){ modal.hidden = true; document.removeEventListener('focus', enforceTrap, true); if(previouslyFocused) previouslyFocused.focus(); }

Opmerking: voor productie gebruik een goed geteste focus-trap library (bijv. focus-trap) of onze plugin die dit standaard regelt. Test met onze plugin.

Roving tabindex voor custom radio/tabcomponenten

<div role="tablist">
  <button role="tab" aria-selected="true" tabindex="0">Tab 1</button>
  <button role="tab" aria-selected="false" tabindex="-1">Tab 2</button>
</div>

/* JS */
const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
tabs.forEach(tab => tab.addEventListener('keydown', e => {
  const idx = tabs.indexOf(tab);
  if(e.key === 'ArrowRight'){ const next = tabs[(idx+1)%tabs.length]; updateFocus(next); }
  if(e.key === 'ArrowLeft'){ const prev = tabs[(idx-1+tabs.length)%tabs.length]; updateFocus(prev); }
  if(e.key === 'Home'){ updateFocus(tabs[0]); }
  if(e.key === 'End'){ updateFocus(tabs[tabs.length-1]); }
}));
function updateFocus(target){ tabs.forEach(t=>{t.tabIndex = -1; t.setAttribute('aria-selected','false')}); target.tabIndex = 0; target.setAttribute('aria-selected','true'); target.focus(); }

Keyboard handlers: Enter en Space

element.addEventListener('keydown', e => {
  if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); element.click(); }
});

Verbeterde toegankelijkheid voor links die modals openen

<a href="#0" role="button" aria-haspopup="dialog" aria-expanded="false">Open details</a>
/* update aria-expanded atrribute bij openen/sluiten */

Checklist voor developers

  • Gebruik semantische elementen: vervang divs door <button> of <a> waar relevant.
  • Voeg keyboard handlers toe voor custom controls (Enter/Space) en voorkom duplicate click handlers.
  • Implementeer focustrap in modals en herstel focus bij sluiten.
  • Gebruik roving tabindex voor groepen (tabs, radios, menu’s).
  • Toon duidelijke focusstijl en test bij verschillende zoomniveaus en high-contrast modi.
  • Vermijd tabindex>0 op veel elementen; beperk tot noodzakelijke cases.
  • Documenteer ARIA-roles en states in componentenbibliotheek.
  • Automatiseer tests met axe-core/cypress-axe en draai een check met onze WCAG checker.

Tips voor designers en redacties

Visible focus en designsystem

Ontwerp duidelijk zichtbare focusstaten (kleur + outline-offset) en voeg deze op componentniveau toe in het designsystem. Vermijd het verbergen van focus door outline: none.

Geen reliance op hover

Zorg dat informatie die op hover wordt getoond ook via focus toegankelijk is (toetsenbord, screenreader). Gebruik :focus-visible om nuances te maken.

Redactie: linkteksten en interactieve content

  • Gebruik unieke, beschrijvende linkteksten (geen “klik hier”).
  • Vermijd inline scripts die autofocus toepassen op paginalaad (storend voor toetsenbord-/screenreadergebruikers).
  • Markeer statusupdates via ARIA live regions voor dynamische content.

Voorbeeld: aria-live voor statusmeldingen

<div id="status" aria-live="polite" aria-atomic="true"></div>
function showStatus(msg){ document.getElementById('status').textContent = msg; }

Hoe test je dit?

Handmatige toetsenbordtest (stappen)

  1. Schakel muis weg en navigeer uitsluitend met Tab / Shift+Tab. Alle interactieve elementen moeten bereikbaar zijn in logische volgorde.
  2. Activeer knoppen en links met Enter/Space; controleer of gedrag identiek is aan click.
  3. Open modals en controleer: focus zit in de modal, Tab blijft binnen modal, Esc sluit modal, bij sluiten wordt focus teruggezet naar opener.
  4. Controleer roving-tabindex: Arrow keys moeten navigeren in tablists/multiselects volgens ARIA authoring practices.
  5. Verhoog zoom en zet hoge contrastmodus aan; controleer zichtbare focus en layoutbreuk.

Screenreader-tests

  • Windows+NVDA: navigeer met Tab en gebruik NVDA-speech om ARIA-labels/rollen te verifiëren.
  • Mac+VoiceOver: controleer navigatie, labels en of dynamic content wordt aangekondigd.

Automated tests: axe-core voorbeeld

// jest + jest-axe voorbeeld
import React from 'react';
import {render} from '@testing-library/react';
import {axe} from 'jest-axe';
test('component is accessible', async ()=>{ const {container} = render(<MyComponent />); const results = await axe(container); expect(results).toHaveNoViolations(); });

E2E met cypress-axe

// cypress support
import 'cypress-axe';
cy.visit('/pagina');
cy.injectAxe();
cy.checkA11y(null, {includedImpacts: ['critical','serious']});

Onze tools

Start met een scan op onze WCAG checker voor snelle feedback. Installeer de WCAGTool plugin voor CI-integratie en automatische rapporten. Voor vragen: contactformulier — antwoorden binnen 24 uur.

Laatste praktische tip

Voeg deze kleine utiliteit toe aan je project om snel toetsenbordvriendelijkheid te testen: een debug-overlay die keyboard-focus en tabindex laat zien.

// debug script: markeert focusable elementen
(function(){const focusables='a,button,input,select,textarea,[tabindex]'; const outlineCSS='2px dashed rgba(255,0,0,0.8)'; const els = Array.from(document.querySelectorAll(focusables)); els.forEach(e=>{if(e.tabIndex <= -1) return; e.style.boxShadow = e.style.boxShadow ? e.style.boxShadow + ', ' + outlineCSS : '0 0 0 3px rgba(255,0,0,0.15)'; e.dataset.__focusable = true;}); console.log('Focusable elements highlighted:', els.length);})();

Test je website nu direct met onze WCAG checker, installeer de plugin voor automatische controles in CI en stuur vragen via het contactformulier — we reageren binnen 24 uur.