Modals en dialogs zijn in theorie simpel, in de praktijk vaak fout: geen focus-trap, achtergrond niet verborgen voor screenreaders, geen aria-modal, geen terugzetten van focus naar de trigger en slechte toetsenbordbediening. Dat leidt direct tot ontoegankelijke flows en gefrustreerde gebruikers.
Wij lossen dit praktisch op met duidelijke codepatronen, testbare snippets en checklists die developers, designers en redacteuren onmiddellijk kunnen toepassen. Test meteen met onze WCAG checker en download onze plugin; vragen via het contactformulier beantwoorden we binnen 24 uur.
Het probleem in de praktijk
Veelvoorkomende fouten bij modals:
- Geen role=”dialog” of aria-modal=”true”.
- Focus wordt niet in het dialog gehouden (Tab verlaat de modal).
- Achtergrondcontent blijft focusable of leesbaar voor screenreaders.
- Geen duidelijke focus-stijlen of onduidelijke sluitknop-labels.
- Dialog niet via toetsen sluitbaar (Esc) of geen terugzetten van focus.
Waarom dit vaak misgaat
Dev-tijd is schaars, frameworks leveren ‘snelle’ oplossingen zonder ARIA, en designers leveren visuals zonder te specificeren focus states en fallback flows. Redacties plaatsen links in modalcontent zonder denken aan keyboardgebruikers.
Zo los je dit op in code
Basismarkup (HTML + ARIA)
Gebruik semantische elementen + ARIA-attributes. Voorbeeld markup:
<!-- Trigger -->
<button id="openModal" aria-haspopup="dialog" aria-controls="myModal">Open dialoog</button>
<!-- Modal -->
<div id="myModal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-describedby="modalDesc" hidden>
<div class="modal__content">
<h2 id="modalTitle">Modal titel</h2>
<p id="modalDesc">Korte beschrijving van de modal-inhoud.</p>
<button class="modal__close" aria-label="Sluit dialoog">×</button>
<!-- inhoud -->
</div>
</div>
CSS: overlay, zichtbare focus en toegankelijkheid
Belangrijk: focus zichtbaar en achtergrond visueel en semantisch verbergen:
.modal__overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:1000}
.modal__content{background:#fff;padding:1.25rem;max-width:600px;border-radius:6px;outline:none}
.modal__close:focus,.modal__content :focus{box-shadow:0 0 0 3px rgba(21,156,228,0.4)}
[hidden]{display:none!important}
JavaScript: focus trap, aria-hidden en focus terugzetten
Gebruik een simpele focus-trap, zet achtergrond op aria-hidden=true en onthoud de opener.
const openBtn=document.getElementById('openModal');
const modal=document.getElementById('myModal');
const closeBtn=modal.querySelector('.modal__close');
let lastFocused=null;
const focusableSelector='a[href],area[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),iframe,object,embed,[tabindex]:not([tabindex="-1"])';
function openModal(){
lastFocused=document.activeElement;
modal.removeAttribute('hidden');
document.body.querySelectorAll('main,header,footer,nav').forEach(el=>el.setAttribute('aria-hidden','true'));
modal.setAttribute('aria-modal','true');
const focusable=Array.from(modal.querySelectorAll(focusableSelector));
focusable[0]?.focus();
document.addEventListener('keydown',trapTabKey);
}
function closeModal(){
modal.setAttribute('hidden','');
document.body.querySelectorAll('main,header,footer,nav').forEach(el=>el.removeAttribute('aria-hidden'));
document.removeEventListener('keydown',trapTabKey);
lastFocused?.focus();
}
function trapTabKey(e){
if(e.key==='Escape'){closeModal();return;}
if(e.key!=='Tab') return;
const focusable=Array.from(modal.querySelectorAll(focusableSelector)).filter(el=>el.offsetParent!==null);
if(focusable.length===0){e.preventDefault();return;}
const first=focusable[0];const last=focusable[focusable.length-1];
if(!modal.contains(document.activeElement)){first.focus();e.preventDefault();return;}
if(e.shiftKey && document.activeElement===first){last.focus();e.preventDefault();}
else if(!e.shiftKey && document.activeElement===last){first.focus();e.preventDefault();}
}
openBtn.addEventListener('click',openModal);
closeBtn.addEventListener('click',closeModal);
Shortcuts en extra toegankelijkheid
Voeg Esc sluiting, focus op eerste focusable en aria-labels toe. Schakel animaties uit voor reduced-motion:
@media (prefers-reduced-motion: reduce){.modal__content{transition:none}}
// In JS: respect reduced-motion als er animaties zijn
Checklist voor developers
- role=”dialog” en aria-modal=”true” aanwezig.
- aria-labelledby en aria-describedby correct gekoppeld.
- Focus-trap werkt: Tab en Shift+Tab blijven in modal.
- Esc sluit de modal en sluitknop heeft duidelijke aria-label.
- Achtergrond krijgt aria-hidden=”true” of inert (waar ondersteund).
- Trigger krijgt focus terug na sluiten.
- Geen focusable elementen buiten modal terwijl open.
- Zichtbare focus-styles en kleurcontrast voldoen aan WCAG AA.
Snelle developer-taken
- Voeg automatisch aria-hidden toe aan
,
- Gebruik querySelectorAll met focusableSelector zoals hierboven en filter op offsetParent.
- Test zonder muis (alle acties via keyboard).
Tips voor designers en redacties
Designers
- Specificeer focus states: kleur, dikte, afstand en hover/active.
- Maak modals responsief: mobile full-screen dialog met consistente close-knop linksboven.
- Beperk animatieduur, respecteer prefers-reduced-motion.
Redacties
- Gebruik korte, duidelijke titels (aria-labelledby) en beschrijvingen (aria-describedby).
- Zorg dat alle interactieve elementen duidelijke labels hebben en vermijd onnodige links in modals.
- Voeg instructie-tekst toe als meerdere stappen in de modal nodig zijn.
Hoe test je dit?
Handmatige toetsenbordtest
- Open de modal via keyboard (Enter/Space op de trigger).
- Druk 5x Tab en 5x Shift+Tab: focus moet binnen modal blijven.
- Druk Escape: modal sluit en focus gaat terug naar trigger.
- Probeer elementen buiten modal te bereiken; dat mag niet.
Screenreader-test (NVDA / VoiceOver)
- Open modal, luister naar announce: titel moet worden voorgelezen door de screenreader.
- Probeer content buiten modal te navigeren; die moet niet toegankelijk zijn.
Automated / E2E test example (Playwright)
import {test,expect} from '@playwright/test';
test('modal trap en sluiten',async({page})=>{
await page.goto('https://jouwsite.example');
await page.focus('#openModal');
await page.keyboard.press('Enter');
// Eerste focus moet binnen modal zijn
const focused=await page.evaluate(()=>document.activeElement.id||document.activeElement.className);
expect(focused).not.toBe('openModal');
// Tab rondom trap testen
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
// Escape sluit
await page.keyboard.press('Escape');
await expect(page.locator('#myModal')).toBeHidden();
});
Gebruik onze tools
Test je pagina direct met onze WCAG checker/validator op wcagtool.nl. Download onze plugin voor geautomatiseerde scans en integreer checks in CI. Vragen? Gebruik ons contactformulier — we reageren binnen 24 uur.
Extra implementatie-tricks en veelgemaakte valkuilen
Gebruik inert waar mogelijk
Moderne browsers ondersteunen inert om achtergrondinteractie te blokkeren. Polyfill waar nodig.
// Voorbeeld: achtergrond inert maken
document.querySelectorAll('main,nav,footer').forEach(el=>el.inert=true);
// fallback: aria-hidden='true'
Vermijd role=”presentation” voor belangrijke elementen
Gebruik role only als element echt geen semantiek heeft. Zet geen role=”presentation” op elkaar van interactieve elementen.
Extra testcases om direct te draaien
- Open modal, zet beeldschermlezer aan, navigeer naar close button met keyboard en activeer.
- Controleer contrast van close-button icon en focus-ring (AA).
- Controleer dat modal-titel binnen 1 woord het doel communiceert.
Test direct met onze WCAG checker op wcagtool.nl, en download onze plugin voor VSCode en CI-integratie zodat elke PR automatisch wordt gescreend. Voor implementatiehulp of code-review: gebruik ons contactformulier — we antwoorden binnen 24 uur.
Praktische tip die je nu kunt toepassen: voeg direct deze regel toe na het openen van de modal om achtergrond onzichtbaar te maken voor schermlezers:document.querySelectorAll('main,nav,footer').forEach(el=>el.setAttribute('aria-hidden','true'));
Test daarna met onze WCAG checker en deel je URL via het contactformulier voor een gratis snelle review binnen 24 uur.