Nederlandse wetgeving rondom digitale toegankelijkheid

WCAG praktisch: concrete implementatiegids – wcagtool.nl

Intro: In de praktijk gaan veel WCAG-implementaties fout omdat teams conceptueel weten wat moet, maar niet weten hoe ze dat betrouwbaar en herhaalbaar in code moeten toepassen. Vaak ontbreken concrete patterns voor keyboard, focus, labels, ARIA en contrast, en worden tijdelijke fixes toegepast zonder regressietests.

Wij vertalen WCAG naar direct toepasbare stappen, werkende codevoorbeelden en testbare checklists zodat developers, frontend engineers, UX/UI designers en redacties meteen resultaat zien. Test je site direct met onze WCAG checker, download de plugin op wcagtool.nl/plugin en neem contact op via wcagtool.nl/contact (vragen binnen 24 uur beantwoord).

Het probleem in de praktijk

Gebrek aan consistente patronen

Teams gebruiken ad-hoc oplossingen: skip-links ontbreken, focus is onzichtbaar, ARIA-attributes verkeerd toegepast en kleurcontrast wordt ad hoc gekozen. Daardoor ontstaan accessibility-regressies bij kleine UI-wijzigingen.

Formulieren en toegankelijke naamgeving

Labels ontbreken of zijn visueel verborgen zonder correcte aria-koppeling; complexere controls (autocomplete, datepickers) missen accessible name en keyboard-ondersteuning.

Focus- en modaalbeheer

Modal-dialogs sluiten zonder focusterugkeer, of focus raakt buiten het zicht; keyboard users stranden. Wij leveren patronen die dit structureel oplossen.

Zo los je dit op in code

1. Skip-link patroon (HTML + CSS)

<a class="skip-link" href="#main">Direct naar inhoud</a>
<!-- CSS -->
.skip-link{position:absolute;left:-999px;top:auto;width:1px;height:1px;overflow:hidden} .skip-link:focus{position:static;left:auto;width:auto;height:auto;padding:8px;background:#fff;color:#000;border:2px solid #000;z-index:1000}

2. Toegankelijke focus-styling (focus-visible polyfill)

/* CSS */
:focus{outline:none}
:focus-visible{outline:3px solid Highlight;outline-offset:2px}
/* fallback voor browsers zonder :focus-visible via JS */
(function(){try{document.documentElement.classList.add('js')}catch(e){}})()

3. Formulieren: labels, aria-describedby en required

<label for="email">E‑mail</label>
<input id="email" name="email" type="email" required aria-describedby="email-help">
<div id="email-help">We gebruiken je e‑mail alleen om in te loggen.</div>

4. Accessible name en role voor custom controls

<div role="button" tabindex="0" aria-pressed="false" aria-label="Favoriet toevoegen">★</div>
/* JS: toggle */
document.querySelectorAll('[role="button"][tabindex]').forEach(el=>el.addEventListener('keydown',e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();el.setAttribute('aria-pressed',el.getAttribute('aria-pressed')!=='true')}}))

5. Modal patroon: trap focus en restore on close

<!-- HTML -->
<button id="openModal">Open modal</button>
<div id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
  <h2 id="modalTitle">Titel</h2>
  <button id="closeModal">Sluiten</button>
</div>
/* JS: focus trap rudimentair */
const open=document.getElementById('openModal'),modal=document.getElementById('modal'),close=document.getElementById('closeModal');open.addEventListener('click',()=>{modal.hidden=false;const focusable=[...modal.querySelectorAll('a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])')];focusable[0]?.focus();document.body.style.overflow='hidden'});close.addEventListener('click',()=>{modal.hidden=true;open.focus();document.body.style.overflow='auto'});document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!modal.hidden){close.click()}})

6. Contrast check functie (JS) — direct testbaar

function luminance(r,g,b){[r,g,b]=[r,g,b].map(v=>{v=v/255;return v<=0.03928? v/12.92: Math.pow((v+0.055)/1.055,2.4)});return 0.2126*r+0.7152*g+0.0722*b}function contrast(hex1,hex2){const h1=hex1.replace('#',''),h2=hex2.replace('#','');const rgb1=[parseInt(h1.substr(0,2),16),parseInt(h1.substr(2,2),16),parseInt(h1.substr(4,2),16)];const rgb2=[parseInt(h2.substr(0,2),16),parseInt(h2.substr(2,2),16),parseInt(h2.substr(4,2),16)];const L1=luminance(...rgb1),L2=luminance(...rgb2);const ratio=(Math.max(L1,L2)+0.05)/(Math.min(L1,L2)+0.05);return ratio}console.log('Contrast #fff / #1a1a1a =',contrast('#ffffff','#1a1a1a'))

Checklist voor developers

  • Semantic HTML overal: buttons <button>, links <a>, headings in volgorde.
  • Alle interactieve elementen zijn focusable en bedienen met Enter en Space.
  • Visuele focus is duidelijk (.focus-visible of outline) en niet alleen via color change.
  • Form controls hebben <label for> of aria-label/aria-labelledby.
  • Contrast minimaal 4.5:1 voor normale tekst, 3:1 voor grote tekst; test via onze checker.
  • Skip-link aanwezig en zichtbaar bij focus.
  • Modals trapten focus en herstellen focus op sluiten.
  • Foutmeldingen focusen en gebruiken aria-describedby/aria-invalid.
  • Gebruik aria-live voor dynamische content (role=alert of aria-live=polite) waar nodig.

Tips voor designers en redacties

Kleur en typografie

Werk met CSS-variabelen voor kleuren zodat contrast snel aangepast kan worden: –brand: #006; –text: #222; body{color:var(–text)}

Componentbibliotheek

Leg vaste patterns vast: accessible buttons, formfields, modals en tooltips. Test elke release met onze plugin en CI-integratie van onze WCAG plugin.

Redactionele guidelines

Alt-teksten: kort + context. Koppen: hiërarchie logisch, maximaal één H1 per pagina. Gebruik linktekst die betekenisvol is zonder de context (“Lees meer” = slecht).

Hoe test je dit?

Automated + manual

Gebruik onze WCAG checker voor snelle scans; combineer met axe-core en Lighthouse in CI. Maar automated tests vangen ~30–40% van issues — maak keyboard- en screenreader-checks standaard.

Keyboard-only test

  1. Navigeer vanaf de URL-balk met Tab en Shift+Tab; alle interactieve controls bereiken en bedienen.
  2. Skip-link werkt; modals sluiten met Escape en focus keert terug.

Screenreader test

Test met NVDA (Windows) of VoiceOver (macOS/iOS): zorgen dat headings en link-tekst zinnig zijn, form controls aangekondigd worden en live-regions voor updates.

Contrast testen

Doe snelle check met de JS-functie hierboven of gebruik onze checker; pas kleuren aan via CSS-variabelen en documenteer keuzes in design tokens.

CI en pre-merge

Integreer de wcagtool plugin en axe-cli in pipelines; zet failures voor nieuwe critical/major issues.

Praktische mini-how-to’s

ARIA-rollen juist gebruiken

Gebruik role alleen als er geen semantic element mogelijk is. Voeg aria-label toe voor naam en aria-labelledby om naar bestaande koppen te refereren.

Visueel verbergen zonder toegankelijkheidsverlies

.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;border:0;clip:rect(0 0 0 0);overflow:hidden;white-space:nowrap}

Fouten inline tonen en focusen

// Bij validatie fout tonen en focusen
const input=document.getElementById('email');const error=document.getElementById('email-error');if(!input.value){error.textContent='Vul een geldig e‑mailadres in';input.setAttribute('aria-invalid','true');input.setAttribute('aria-describedby','email-error');error.focus();}

Checklijst voor implementatie (kopieerbaar)

  1. Voeg skip-link en test met keyboard.
  2. Controleer alle interactieve elementen op semantiek en tabindex.
  3. Implementeer :focus-visible of vergelijkbaar.
  4. Valideer kleurcontrast met JS-tooling en onze checker.
  5. Label alle form controls; gebruik aria-describedby voor hulp en foutmelding.
  6. Voer screenreader- en keyboard-sessies uit per pagina type.
  7. Integreer automated scans met onze plugin in CI.

Hoe wij ondersteunen

Gebruik onze WCAG checker voor directe resultaten; download de plugin voor browser- en CI-integratie. Vragen? Stuur ze via wcagtool.nl/contact — we reageren binnen 24 uur.

Laatste praktische tip: plak dit korte audit-script in de console van je pagina om direct 5 checks uit te voeren: aanwezigheid van skip-link, body role/main, focusable elementen, contrast check van body/background en of modals aria-modal gebruiken. Kopieer en run:

(function(){console.log('skip-link?',!!document.querySelector('.skip-link'||'a[href^="#"'));// skip-link check
console.log('main landmark?',!!document.querySelector('main')||!!document.querySelector('[role="main"]'));// main landmark
console.log('focusable count:',document.querySelectorAll('a,button,input,select,textarea,[tabindex]:not([tabindex="-1"])').length);// focusables
try{const bg=getComputedStyle(document.body).backgroundColor,fg=getComputedStyle(document.body).color;console.log('contrast body/bg->fg',contrast(rgbToHex(fg),rgbToHex(bg)));}catch(e){console.log('contrast check skipped')}function rgbToHex(rgb){const m=rgb.match(/\\d+/g)||[];return'#'+m.slice(0,3).map(n=>('0'+parseInt(n).toString(16)).slice(-2)).join('')}function luminance(r,g,b){[r,g,b]=[r,g,b].map(v=>{v=v/255;return v<=0.03928? v/12.92: Math.pow((v+0.055)/1.055,2.4)});return 0.2126*r+0.7152*g+0.0722*b}function contrast(hex1,hex2){const h1=hex1.replace('#',''),h2=hex2.replace('#','');const rgb1=[parseInt(h1.substr(0,2),16),parseInt(h1.substr(2,2),16),parseInt(h1.substr(4,2),16)];const rgb2=[parseInt(h2.substr(0,2),16),parseInt(h2.substr(2,2),16),parseInt(h2.substr(4,2),16)];const L1=luminance(...rgb1),L2=luminance(...rgb2);return (Math.max(L1,L2)+0.05)/(Math.min(L1,L2)+0.05)} })();

Test je website nu met onze WCAG checker, download de plugin op wcagtool.nl/plugin en stel je vragen via wcagtool.nl/contact — antwoord binnen 24 uur.