Toegankelijke formulieren: praktisch toepassen (labels, fouten, focus, ARIA)
Formulieren falen in de praktijk vaak doordat ontwikkelaars vertrouwen op visuele aanwijzingen en browser-standaardvalidatie zonder expliciete labels, foutkoppeling en focusbeheer. Resultaat: onvindbare foutmeldingen, slecht gekoppelde labels en een slechte ervaring voor toetsenbord- of screenreadergebruikers.
Wij lossen dit op met checklists, kant-en-klare code-snippets en testbare stappen die developers, designers en redacties direct kunnen inzetten. Test je site nu met onze WCAG checker, download onze plugin (plugin) en stel je vraag via het contactformulier — we reageren binnen 24 uur.
Het probleem in de praktijk
Veelvoorkomende fouten
- Ontbrekende of niet-gekoppelde <label>-elementen.
- Foutmeldingen alleen visueel weergegeven (kleur, icon) zonder aria-verbinding of live region.
- Geen focus-management na submit of na dynamische validatie.
- Gebruik van rollen/ARIA in plaats van native controls.
- Inconsistente required-indicatoren en onduidelijke instructies voor content-editors.
Waarom dat leidt tot WCAG-overtredingen
Deze fouten raken meerdere WCAG-criteria: name-role-value, labels, error identification (3.3.1 en 3.3.3), focus visibility (2.4.7) en keyboard toegankelijkheid. Praktisch gezegd: gebruikers weten niet waar ze fouten moeten herstellen en screenreaders krijgen geen semantische cues.
Zo los je dit op in code
Basisstructuur: HTML-formulier met correcte labels en foutkoppeling
<form id="contactForm" novalidate>
<div class="form-row">
<label for="email">E-mailadres</label>
<input id="email" name="email" type="email" aria-describedby="email-error" required />
<div id="email-error" class="error" aria-live="polite"><!-- dynamische foutmelding --></div>
</div>
<div class="form-row">
<label for="message">Bericht</label>
<textarea id="message" name="message" aria-describedby="message-error" required></textarea>
<div id="message-error" class="error" aria-live="polite"></div>
</div>
<button type="submit">Verstuur</button>
</form>
Stap-voor-stap: validatie en fout-annotatie met JavaScript
const form = document.getElementById('contactForm');
form.addEventListener('submit', function(e){
e.preventDefault();
const email = form.elements['email'];
const message = form.elements['message'];
clearErrors();
let hasError = false;
if(!email.value || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
setError(email, 'Vul een geldig e-mailadres in.');
hasError = true;
}
if(!message.value.trim()) {
setError(message, 'Schrijf een kort bericht.');
hasError = true;
}
if(hasError){
const firstError = form.querySelector('[aria-invalid="true"]');
firstError.focus();
return;
}
// stuur formulier via fetch of normale submit
submitForm();
});
function setError(control, message){
const id = control.getAttribute('id');
const errorEl = document.getElementById(id + '-error');
control.setAttribute('aria-invalid', 'true');
control.setAttribute('aria-describedby', id + '-error');
errorEl.textContent = message;
errorEl.classList.add('visible');
}
function clearErrors(){
form.querySelectorAll('[aria-invalid="true"]').forEach(el => el.removeAttribute('aria-invalid'));
form.querySelectorAll('.error').forEach(el => { el.textContent = ''; el.classList.remove('visible'); });
}
CSS: zichtbare focus en toegankelijke foutstijl
.form-row input:focus, .form-row textarea:focus{ outline: 3px solid #005fcc; outline-offset: 2px; }
.error{ color:#a00; font-size:0.95rem; min-height:1.2em; }
.error.visible{ display:block; }
.sr-only{ position:absolute; left:-9999px; }
ARIA-praktijken: wanneer wel/niet gebruiken
- Gebruik native <label> en semantische controls waar mogelijk. ARIA is geen vervanging.
- Gebruik aria-describedby om foutteksten en extra instructies te koppelen.
- Gebruik aria-invalid=”true” op het formulier-element dat fout is.
- Gebruik aria-live=”polite” op foutcontainers voor dynamische updates; gebruik role=”alert” alleen voor urgente meldingen.
Checklist voor developers
- Elke input heeft een <label for=”id”> of aria-label/aria-labelledby als label niet zichtbaar is.
- Fouten zijn gekoppeld via aria-describedby en worden dynamisch gevuld.
- Na submit wordt focus verplaatst naar het eerste foutveld (focus-management).
- Visuele focus is duidelijk (outline/box-shadow), niet alleen kleur.
- Gebruik native validation waar mogelijk, maar voeg toegankelijke foutafhandeling toe voor custom validation.
- Controleer tabindex-orde; voorkom tabindex>0.
- Test met toetsenbord, screenreader en onze WCAG checker.
Tips voor designers en redacties
Design: visuele en tekstuele cues
- Combineer iconen met tekstlabels; kleur alleen is geen indicatie.
- Zorg dat foutteksten kort, specifiek en benoembaar zijn (“E-mailadres heeft geen @-teken”).
- Maak foutmeldingen consistent qua positie (b.v. onder het form field)
Redactie: voorbeeldteksten en contentregels
- Stel templates beschikbaar met standaard foutmeldingen en instructies.
- Vermijd jargon; geef concrete actie-instructies (bijv. “Vul een geldig e-mailadres in zoals naam@voorbeeld.nl”).
- Gebruik duidelijke required-indicatoren en omschrijf validatieregels als helpertekst.
Wil je voorbeeldcontent en ready-to-use snippets voor redacties? Download onze plugin op wcagtool.nl/plugin of vraag hulp via het contactformulier (antwoord binnen 24 uur).
Hoe test je dit?
Handmatige tests (snel en effectief)
- Toetsenbord-only: navigeer zonder muis, focus moet logisch en zichtbaar zijn; probeer velden leeg in te sturen.
- Schermlezer: test met NVDA (Windows) en VoiceOver (macOS/iOS). Check of labels, foutmeldingen en focus zowel gesproken als logisch zijn.
- Visuele check: zet browsercontrastlaag aan en controleer focus/contrast van fouttekst.
Automated tests en CI
Integreer axe-core in je CI of run Lighthouse. Voor end-to-end: Playwright-voorbeeld dat controleert op aria-describedby en focus verplaatsing:
const { test, expect } = require('@playwright/test');
test('form shows error and focuses first invalid field', async ({ page }) => {
await page.goto('https://jouwsite.nl/testform');
await page.click('button[type=submit]');
const firstInvalid = await page.locator('[aria-invalid="true"]').first();
await expect(firstInvalid).toBeVisible();
await expect(page.evaluate(() => document.activeElement.id)).resolves.toBe(await firstInvalid.getAttribute('id'));
});
Gebruik onze tools
Run direct een quick-scan met onze WCAG checker of installeer de browserplugin voor inline feedback tijdens development. Bij vragen: contactformulier — we reageren binnen 24 uur.
Extra praktische how-to’s
Inline validatie zonder annoyance
Valideer bij blur en op submit; gebruik aria-live=”polite” zodat screenreaders foutmeldingen niet onderbreken. Voorbeeld:
input.addEventListener('blur', e => { validateField(e.target); });
function validateField(field){
// korte check en setError/clearErrors zoals eerder beschreven
}
Complexe widgets (datepickers, custom selects)
- Gebruik role=”combobox” en aria-expanded/aria-controls correct, maar probeer native <select> eerst.
- Zorg dat keyboard navigation (Arrow keys, Home/End) werkt en dat selectie gerapporteerd wordt via aria-activedescendant.
Server-side fouten
Bij server-side validatie geef je na response de foutkoppeling terug in JSON met de field-id’s en zet je aria-invalid + focus op het eerste veld. Voorbeeld response-handler:
fetch('/submit',{method:'POST',body:formData}).then(r => r.json()).then(data => {
if(data.errors){
data.errors.forEach(err => setError(document.getElementById(err.field), err.message));
document.querySelector('[aria-invalid="true"]').focus();
} else { /* succes */ }
});
Laatste praktische tip
Voeg aan elk formulier een eenduidige testknop toe in je staging-omgeving die alle fouten forceert, en test deze met onze WCAG checker en de plugin. Als je vastloopt: stuur je URL via het contactformulier (antwoord binnen 24 uur) of start direct een scan op wcagtool.nl.