Formulieren, keyboard-navigatie en focusbeheer falen in de praktijk vaak omdat ontwikkelaars visuele aannames maken: labels die alleen met CSS zichtbaar zijn, foutmeldingen die niet naar de assistive technology worden gepushed, of custom controls zonder correcte ARIA. Dit levert onbruikbare interfaces voor keyboard- en screenreader-gebruikers.
Wij vertalen WCAG-criteria naar direct toepasbare code, stap-voor-stap testplannen en checklisten zodat jouw site of app binnen één sprint meetbaar toegankelijk wordt. Test direct met onze WCAG checker/validator, download onze plugin en stel vragen via ons contactformulier (antwoord binnen 24 uur).
Het probleem in de praktijk
Veelvoorkomende fouten
- Labels niet gekoppeld aan inputs (geen for/id of aria-label)
- Foutmeldingen alleen visueel zonder aria-live of focus-management
- Custom controls zonder keyboard support of juiste role/aria-states
- Focus styles verwijderd via outline:none zonder alternatief
- Gebrek aan semantische HTML (buttons als divs, poor headings)
Gevolgen voor gebruikers
Gebruikers kunnen niet formulieren invullen, foutmeldingen missen of vastlopen in modals/menus. Dit leidt tot drop-off, klachten en juridische risico’s.
Zo los je dit op in code
Basisform: semantisch, labels en required
<form id="contactForm">
<label for="name">Naam</label>
<input id="name" name="name" type="text" required aria-required="true" />
<label for="email">E-mail</label>
<input id="email" name="email" type="email" required aria-required="true" />
<button type="submit">Verzenden</button>
<div id="formStatus" aria-live="polite" aria-atomic="true"></div>
</form>
Valideer client-side, zet ARIA-states
document.getElementById('contactForm').addEventListener('submit', function(e){
e.preventDefault();
const name = document.getElementById('name');
const email = document.getElementById('email');
let valid = true;
clearErrors();
if(!name.value.trim()){ setError(name, 'Vul je naam in'); valid = false; }
if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)){ setError(email, 'Ongeldig e-mailadres'); valid = false; }
if(!valid){
// focus naar eerste fout
const firstError = document.querySelector('[aria-invalid="true"]');
firstError.focus();
return;
}
// verstuur en update aria-live
document.getElementById('formStatus').textContent = 'Versturen...';
// ajax call...
});
function setError(input, message){
input.setAttribute('aria-invalid','true');
input.setAttribute('aria-describedby', input.id + '-err');
const err = document.createElement('div');
err.id = input.id + '-err';
err.className = 'form-error';
err.textContent = message;
err.setAttribute('role','alert'); // optioneel ivm aria-live
input.after(err);
}
function clearErrors(){
document.querySelectorAll('.form-error').forEach(n => n.remove());
document.querySelectorAll('[aria-invalid]').forEach(i => i.removeAttribute('aria-invalid'));
document.querySelectorAll('[aria-describedby]').forEach(i => i.removeAttribute('aria-describedby'));
}
Focusbeheer bij modals en custom components
// Basic focus trap
function trapFocus(container){
const focusable = container.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])');
const first = focusable[0], last = focusable[focusable.length-1];
function handle(e){
if(e.key === 'Tab'){
if(e.shiftKey && document.activeElement === first){ e.preventDefault(); last.focus(); }
else if(!e.shiftKey && document.activeElement === last){ e.preventDefault(); first.focus(); }
}
if(e.key === 'Escape'){ closeModal(); }
}
container.addEventListener('keydown', handle);
// cleanup functie bij sluiten modal
}
Focus styles en reduced-motion
:focus{ outline: 3px solid #005fcc; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce){ *{ transition: none !important; animation: none !important; }}
Accessible custom checkbox (role & states)
<div role="checkbox" tabindex="0" aria-checked="false" id="customCheck">Ik ga akkoord</div>
document.getElementById('customCheck').addEventListener('keydown', function(e){
if(e.key === ' ' || e.key === 'Enter'){ e.preventDefault(); toggle(this); }
});
function toggle(el){
const checked = el.getAttribute('aria-checked') === 'true';
el.setAttribute('aria-checked', String(!checked));
// sync with hidden input voor form submission
}
Checklist voor developers
- Alle form-controls hebben <label for=”id”> of aria-label/aria-labelledby
- Fouten hebben aria-live/role=”alert” en focus verplaatst naar eerste fout
- Custom controls hebben role, tabindex, keyboard handlers en aria-checked/aria-selected
- Modal/overlay: focus trap, restore focus bij sluiten, escape sluiten
- Geen visual-only labels, alt-text voor images, en semantische buttons
- Contrast >= 4.5:1 voor body tekst; controle met onze checker
- Automatische toetsen: axe-core / jest-axe in CI; voorbeeld hieronder
CI-integratie met axe-core (voorbeeld)
const { axe, toHaveNoViolations } = require('jest-axe');
expect.extend(toHaveNoViolations);
test('Home page should have no accessibility violations', async () => {
const html = renderToString(<App />);
const results = await axe(html);
expect(results).toHaveNoViolations();
});
Tips voor designers en redacties
Design tokens voor focus en kleur
Definieer globale tokens: –color-focus, –color-error, –font-size-base. Zorg dat focus niet weggepoetst wordt in designs en leg states vast in component-library (focus-visible, hover, disabled)
Content: beknopt, beschrijvend en consistent
- Kopteksten: hiërarchie H1-H2-H3 consistent; geen styling-only headings
- Alt-teksten: functioneel en eerlijk, geen “afbeelding van”
- Linktekst: beschrijvend, geen “klik hier”
Formulierteksten en foutmeldingen
Redacteuren moeten korte, actiegerichte foutberichten schrijven (bijv. “Voer een geldig e-mailadres in”) en contextuele hulp (aria-describedby) beschikbaar stellen.
Hoe test je dit?
Handmatige tests — stap voor stap
- Keyboard-only: navigeer zonder muis. Kun je alle elementen bereiken en activeren? Test ook modals/menus.
- Screenreader: test met NVDA/JAWS/VoiceOver. Lees labels, foutmeldingen en dynamic updates (aria-live).
- Contrast: test belangrijke tekstblokken en knoppen met onze checker.
- Form validatie: laat intentional errors zien en controleer focus en aria-attributes.
- Reduced motion: zet systeem op reduced-motion en controleer animaties.
Automatische tests
Integreer axe-core in unit/CI en gebruik Lighthouse en onze WCAG checker voor faalgevallen die menselijke review nodig hebben. Gebruik ook end-to-end toetsen (Playwright/Puppeteer) voor keyboard flows.
Voorbeeld testscript (Playwright)
const { test, expect } = require('@playwright/test');
test('Form keyboard flow en fout', async ({ page }) => {
await page.goto('https://example.com/contact');
await page.keyboard.press('Tab'); // skip-link of eerste element
// navigeer naar naamveld
await page.keyboard.type(''); // lege naam
await page.click('button[type="submit"]');
// controleer focus op fout en aria-invalid aanwezig
const ariaInvalid = await page.locator('[aria-invalid="true"]').first();
await expect(ariaInvalid).toBeVisible();
});
Checklist voor redacties & content-editors
- Elke afbeelding heeft een alt-tekst of role=”presentation” als decoratief
- Links beschrijven bestemming of actie
- Kopstructuur logisch en semantisch
- Gebruik korte, duidelijke foutmeldingen en placeholders als ondersteuning (niet als vervanging voor labels)
Calls-to-action
Test je pagina nu met onze WCAG checker/validator en krijg concrete verbeterpunten. Download onze plugin voor VS Code/Chrome voor inline feedback tijdens development. Heb je vragen? Gebruik ons contactformulier — wij reageren binnen 24 uur.
Praktische tip: voeg direct deze kleine helper toe aan je global JS om aria-live te normaliseren en fouten naar screenreaders te sturen:
// aria-live helper
function announce(message, politeness='polite'){
let live = document.getElementById('__a11y_live_region__');
if(!live){
live = document.createElement('div');
live.id = '__a11y_live_region__';
live.setAttribute('aria-live', politeness);
live.setAttribute('aria-atomic','true');
live.className = 'visually-hidden';
document.body.appendChild(live);
}
live.textContent = '';
setTimeout(()=> live.textContent = message, 50);
}
Gebruik announce(‘Er is een fout bij e-mailveld’) direct na validatiefouten, en test meteen met onze checker.