Verwachte veranderingen in WCAG 3.0

Praktische WCAG-implementatie: Toegankelijke formulieren, focus en ARIA — wcagtool.nl

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

  1. Keyboard-only: navigeer zonder muis. Kun je alle elementen bereiken en activeren? Test ook modals/menus.
  2. Screenreader: test met NVDA/JAWS/VoiceOver. Lees labels, foutmeldingen en dynamic updates (aria-live).
  3. Contrast: test belangrijke tekstblokken en knoppen met onze checker.
  4. Form validatie: laat intentional errors zien en controleer focus en aria-attributes.
  5. 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.