Toegankelijke formulieren en foutafhandeling: praktisch toepasbaar
Formulieren falen vaak omdat labels, foutberichten en focusbeheer los van elkaar worden ontworpen. Dat leidt tot onvindbare fouten voor screenreadergebruikers, onduidelijke instructies voor toetsenbordgebruikers en onleesbare foutmeldingen voor mensen met cognitieve beperkingen. Wij vertalen WCAG-vereisten naar concrete code en testbare stappen zodat implementatie direct werkt in productie.
In dit artikel: duidelijke problemen uit de praktijk, complete code-voorbeelden (HTML/CSS/JS/ARIA), checklists voor developers, tips voor designers en redacties, en gereedschappen om direct te testen met onze WCAG checker. Test je site meteen via https://wcagtool.nl/checker, download onze plugin op https://wcagtool.nl/plugin of stel je vraag via https://wcagtool.nl/contact — we reageren binnen 24 uur.
Het probleem in de praktijk
Veelvoorkomende fouten
- Velden zonder expliciet <label> of met visueel verborgen labels die niet correct zijn gekoppeld.
- Foutmeldingen die los staan van de invoervelden, zonder aria-describedby of aria-invalid.
- Geen focusmanagement bij server- of client-side validatie — schermlezers missen updates.
- Kleur alleen gebruiken om fouten of vereist-status aan te geven zonder extra tekst/icoon.
- Onvoldoende contrast en te kleine fouttekst, onduidelijke instructies.
Waarom dit gebruikers schaadt
Screenreaders hebben duidelijke relaties nodig tussen label, veld en foutmelding. Toetsenbordgebruikers moeten weten waar de focus heen moet na een fout. Zonder duidelijke associaties kunnen gebruikers niet corrigeren en haken ze af.
Zo los je dit op in code
Basis-HTML: labels en beschrijvingen correct koppelen
<form id="signup" novalidate><div class="form-row"><label for="email">E-mailadres<span aria-hidden="true" class="required">*</span></label><input id="email" name="email" type="email" required aria-describedby="email-desc email-error" /><div id="email-desc" class="hint">We sturen geen spam. Gebruik jouw werk- of privéadres.</div><div id="email-error" class="error" aria-live="polite"> </div></div><div class="form-row"><label for="password">Wachtwoord</label><input id="password" name="password" type="password" required aria-describedby="password-error" /><div id="password-error" class="error" aria-live="polite"> </div></div><button type="submit">Aanmelden</button></form>
Client-side validatie met focusmanagement (JavaScript)
document.getElementById('signup').addEventListener('submit', function(e){e.preventDefault();var firstError=null;var errors=[];var email=document.getElementById('email');var password=document.getElementById('password');function setError(el,msg){var errorId=el.getAttribute('aria-describedby').split(' ').pop();var errorEl=document.getElementById(errorId);errorEl.textContent=msg;el.setAttribute('aria-invalid','true');if(!firstError){firstError=el;}}function clearError(el){var descs=el.getAttribute('aria-describedby').split(' ');var errorId=descs[descs.length-1];document.getElementById(errorId).textContent='';el.removeAttribute('aria-invalid');}clearError(email);clearError(password);if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)){setError(email,'Vul een geldig e-mailadres in.');errors.push('email');}if(password.value.length < 8){setError(password,'Wachtwoord moet minimaal 8 tekens zijn.');errors.push('password');}if(errors.length){firstError.focus();}else{ // success: submit via AJAX or plain submit this.submit();}});
CSS: zichtbare focus en foutstijl (accessibility-first)
.form-row{margin-bottom:1rem}.error{color:#b00020;font-size:0.95rem;min-height:1.2rem}.required{color:#b00020;margin-left:0.25rem}.visually-hidden{position:absolute!important;height:1px;width:1px;overflow:hidden;clip:rect(1px,1px,1px,1px);white-space:nowrap}input:focus{outline:3px solid #0366d6;outline-offset:2px}input[aria-invalid="true"]{border:2px solid #b00020;background:#fff5f5}
Server-side foutberichten: scroll & focus na response
// server geeft JSON terug: { errors: [{field: 'email', message: '...' }, ...] }fetch('/api/submit', {method:'POST', body: new FormData(form)}) .then(r=>r.json()) .then(data=>{if(data.errors){var first=null;data.errors.forEach(function(err){var el=document.querySelector('[name="'+err.field+'"]');var descs=el.getAttribute('aria-describedby').split(' ');var errorId=descs[descs.length-1];document.getElementById(errorId).textContent=err.message;el.setAttribute('aria-invalid','true');if(!first) first=el;}if(first){first.focus();window.scrollTo({top:first.getBoundingClientRect().top+window.scrollY-80,behavior:'smooth'});} }else{ // succes }});
Checklist voor developers
- Elke input heeft een <label for=”id”> dat zichtbaar of toegankelijk is.
- Gebruik aria-describedby om hint- en fouttekst aan een veld te koppelen.
- Wijs aria-invalid=”true” toe bij fouten, verwijder het bij correctie.
- Foutcontainers hebben aria-live=”polite” of “assertive” afhankelijk urgentie, en bevatten altijd tekst (geen lege container).
- Focus altijd op het eerste foutieve veld na validatie (client/server) en scroll naar zichtbare positie.
- Niet alleen kleur gebruiken om vereist/fout te tonen; voeg tekst en iconen toe.
- Contrast van fouttekst en iconen minstens 4.5:1 tov achtergrond; bronnen en labels 4.5:1.
- Test met keyboard-only, screenreader (NVDA/VoiceOver), en onze WCAG checker (https://wcagtool.nl/checker).
Tips voor designers en redacties
Ontwerppatronen
- Ontwerp consistente foutplaatsing: fouttekst direct onder het veld vermindert zoekinspanning.
- Gebruik korte, concrete instructies. Bijv. “Wachtwoord minimaal 8 tekens, één cijfer” in de hint (aria-describedby).
- Maak required-icoon en fout-icoon expliciet en voorzien van aria-hidden=”true” en extra tekst voor screenreaders (“verplicht veld”).
Redactie-richtlijnen voor foutteksten
- Gebruik actieve taal: “Vul je e-mailadres in.” in plaats van “E-mailadres ontbreekt.”
- Vermijd technisch jargon; lever voorbeeldinvoer waar relevant.
- Houd foutteksten kort (max 1-2 regels) en voeg link naar help of tooltip toe voor complexe validatie.
Hoe test je dit?
Automatische checks
- Run eerste scan met onze WCAG checker: https://wcagtool.nl/checker. De tool detecteert ontbrekende labels, aria-describedby ontbreken en contrastproblemen.
- Installeer onze browser-plugin: https://wcagtool.nl/plugin voor inline feedback tijdens development.
Handmatige checks (stap-voor-stap)
- Keyboard-only: navigeer naar formulier, probeer te vullen en te verzenden zonder muis. Let op focusvolgorde en zichtbare foutmeldingen.
- Screenreader-test: activeer NVDA of VoiceOver. Navigeer naar veld, lees label, trigger fout en verifieer dat foutmelding wordt voorgelezen en gekoppeld is (aria-describedby of aria-invalid + error container).
- Contrast & schaal: controleer fouttekst en labels met contrasttool. Test zoom tot 200% en controleer dat layout bruikbaar blijft.
- Functionaliteit: corrigeer een fout en controleer dat aria-invalid wordt verwijderd en foutmelding verdwijnt of update. Gebruik browser devtools om attributes te inspecteren.
Concrete testcases
- Submit lege form: verwacht focus op eerste verplicht veld, fout zichtbaar en screenreader-uitspraak.
- Submit ongeldig e-mail: fouttekst “Vul een geldig e-mailadres in.” gekoppeld aan input via aria-describedby.
- Server-side fout (bijv. e-mail reeds in gebruik): foutmelding bovenaan en gekoppeld aan veld+focus op input.
Snelle implementatie-scan (copy-paste)
Plak dit in console om snel te checken of inputs labels en error containers hebben juiste attributes:
(function(){var errors=[];document.querySelectorAll('input,textarea,select').forEach(function(el){if(!el.id)errors.push('Geen id: '+el.name||el.tagName);var hasLabel=!!document.querySelector('label[for="'+el.id+'"]');if(!hasLabel)errors.push('Geen gekoppeld label voor: '+el.id);var desc=el.getAttribute('aria-describedby');if(!desc)errors.push('Geen aria-describedby voor: '+el.id);});if(errors.length)console.warn('Snelle scan gevonden:',errors);else console.log('Basis attributen OK');})();
Extra tips en valkuilen
Gebruik aria-live verstandig
Voor inline foutmeldingen is aria-live=”polite” meestal voldoende; gebruik “assertive” alleen voor urgente, actie-blocking errors. Zorg dat live-regions niet onnodig vaak updaten (spam voor screenreaders).
Vermijd aria-errormessage-deprecated patronen
Gebruik aria-describedby gekoppeld aan een foutcontainer met duidelijke tekst in plaats van oude aria-errormessage-implementaties. Houd IDs consistent en uniek per veld.
Server-side: stuur veldnaam en message terug
API-voorbeeldresponse voor consistente handling: { “errors”:[{“field”:”email”,”message”:”E-mailadres is al in gebruik.”}] } Verwerk deze door foutcontainers te vullen en focus te zetten zoals in het JS-voorbeeld hierboven.
Laat onze tools je helpen
Test direct met onze WCAG checker: https://wcagtool.nl/checker. Download de plugin voor snelle developer-feedback: https://wcagtool.nl/plugin. Vragen over implementatie? Stuur ze via https://wcagtool.nl/contact — we reageren binnen 24 uur en helpen met concrete code-aanpassingen.
Laatste praktische check: gebruik deze korte checklist in je CI/build pipeline of pre-deploy checklist en draai onze checker automatisch. Plaats herinnering in PR-template: “Heeft dit formulier aria-describedby, aria-invalid en focus-management?”
Tiny final code-snippet om aria-invalid automatisch te verwijderen bij input:
document.querySelectorAll('input[aria-invalid="true"],textarea[aria-invalid="true"]').forEach(function(el){el.addEventListener('input',function(){if(el.value.trim()!==''){el.removeAttribute('aria-invalid');var descs=el.getAttribute('aria-describedby');if(descs){var id=descs.split(' ').pop();document.getElementById(id).textContent='';}}});});