Waarom modals en focusbeheer vaak misgaan Modals, dialogs en overlays veroorzaken in praktijk veel toegankelijksheidsproblemen: focus raakt zoek, schermlezers blijven content op de achtergrond lezen of gebruikers kunnen niet via toetsen navigeren. Dit leidt tot onbruikbare flows voor keyboard- en screenreader-gebruikers en tot WCAG-fouten (2.1.1, 2.4.3, 4.1.2).
Wij helpen je stap-voor-stap met implementaties die wérkelijk bruikbaar zijn: heldere HTML-structuren, ARIA-attributes, CSS voor zichtbaarheid en concrete JavaScript-routines voor focus-trapping, restore-focus en het verbergen van achtergrondcontent. Test direct met onze WCAG checker op https://wcagtool.nl/checker, download onze plugin op https://wcagtool.nl/plugin en neem bij vragen contact op via https://wcagtool.nl/contact (antwoord binnen 24 uur).
Het probleem in de praktijk
Wat gaat er fout?
Veelgemaakte fouten: (1) achtergrondcontent blijft focusbaar en zichtbaar voor screen readers; (2) focus wordt niet teruggezet naar de trigger; (3) geen keyboard-escape (Esc); (4) slechte ARIA-rollen of ontbreken van aria-modal/aria-hidden; (5) meerdere modals tegelijk zonder beheer.
Impact op gebruikers
Keyboard-only gebruikers blijven vastzitten of verliezen context. Screenreader-gebruikers krijgen onjuiste semantiek en horen content dubbel. Dit schendt WCAG en leidt tot onbruikbare interfaces.
Zo los je dit op in code
Basis HTML-structuur voor een toegankelijke modal
Gebruik een duidelijke trigger en een dialog-element met role=”dialog” en aria-modal=”true”. Voorbeeldmarkup (gebruik button
voor triggers):
<button id="openModal">Open instellingen</button><div id="overlay" hidden><div id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle"><h2 id="modalTitle">Instellingen</h2><button id="closeModal">Sluiten</button><form><label>Voorkeur<select><option>A</option></select></label></form></div></div>
CSS: zichtbaarheid en visuele focus
Minimaliseer visuele verlies van focus en zorg dat overlay onderscheidend is:
#overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center}#modal{background:#fff;padding:1rem;max-width:600px;outline:0;border-radius:4px}#modal :focus{outline:3px solid #005fcc;outline-offset:2px}
JavaScript: open, trap focus, restore en verberg achtergrond
Belangrijke stappen: bewaar focus, zet aria-hidden op rest van de pagina of gebruik inert, trap focus binnen modal, sluit op Esc, zet focus terug.
(function(){const openBtn=document.getElementById('openModal');const closeBtn=document.getElementById('closeModal');const overlay=document.getElementById('overlay');const modal=document.getElementById('modal');const pageRoot=document.querySelector('main')||document.body;let lastFocused=null;function getFocusable(container){return Array.from(container.querySelectorAll('a[href],button:not([disabled]),input:not([disabled]),textarea:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])')).filter(el=>el.offsetWidth||el.offsetHeight||el.getClientRects().length);}function openModal(){lastFocused=document.activeElement;overlay.hidden=false;pageRoot.setAttribute('aria-hidden','true');modal.setAttribute('tabindex','-1');modal.focus();trapFocus();}function closeModal(){overlay.hidden=true;pageRoot.removeAttribute('aria-hidden');removeTrap();if(lastFocused)lastFocused.focus();}function trapFocus(){const focusables=getFocusable(modal);if(!focusables.length)return;let first=focusables[0];let last=focusables[focusables.length-1];function handleKey(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()}}modal.addEventListener('keydown',handleKey);modal._handleKey=handleKey;}function removeTrap(){if(modal._handleKey)modal.removeEventListener('keydown',modal._handleKey);delete modal._handleKey;}openBtn.addEventListener('click',openModal);closeBtn.addEventListener('click',closeModal);overlay.addEventListener('click',e=>{if(e.target===overlay)closeModal()});})();
Gebruik van inert en polyfill
Prefer inert omdat aria-hidden op complexe apps niet altijd veilig is. Voeg polyfill toe voor oudere browsers: polyfill: https://github.com/WICG/inert
. Voorbeeld gebruik:
import 'wicg-inert';const main=document.querySelector('main');main.inert=true;/* bij sluiten: main.inert=false */
Checklist voor developers
Essentiële punten (snelle checklist)
- role=”dialog” en aria-modal=”true” op het dialog-element
- Bevries achtergrond met
inert
of zet aria-hidden op hoofdcontent - Trap focus binnen de modal (Tab en Shift+Tab)
- Restore focus naar trigger na sluiten
- Sluiten met Escape en click buiten (optioneel) maar altijd met ARIA-updates
- Visible focus styles en juiste focusable elementen
- Screenreader-label via aria-labelledby of aria-label
- Zorg dat er niet meerdere modals tegelijk open kunnen zijn
Code-checks
Voer deze tests uit in je codebase: voeg unit-tests voor trapFocus, e2e-tests die keyboard flow simuleren en accesstests met axe-core geïntegreerd in CI. Gebruik onze WCAG checker/validator op https://wcagtool.nl/checker voor snelle feedback.
Tips voor designers en redacties
Designregels
- Modal breedte en contrast voldoen aan WCAG (tekstcontrasten ≥ 4.5:1 voor kleine tekst)
- Voorkom te veel nested dialogs; gebruik progressieve disclosure in één modal
- Zorg voor duidelijke visuele focus en voldoende hit-area voor knoppen
Content-richtlijnen voor redacties
- Gebruik korte, duidelijke labels voor knoppen (geen ‘Ok’ zonder context)
- Zorg voor een duidelijke titel in aria-labelledby en zichtbare H2
- Vermijd automatische focus switches zonder gebruikerstoestemming
Hoe test je dit?
Handmatige keyboard-test
1) Open modal met Enter/Space op trigger. 2) Tab door elementen en verifieer dat focus niet naar achterliggende pagina gaat. 3) Shift+Tab van eerste element moet naar laatste gaan en omgekeerd. 4) Druk Esc en controleer focus terug op trigger.
Screenreader-test (NVDA / VoiceOver)
1) Activeer screenreader. 2) Open modal en luister of de dialog-naam wordt aangekondigd en of alleen dialog-elements hoorbaar zijn. 3) Sluit dialog en verifieer dat context hersteld wordt.
Automated tests
Integreer axe-core of pa11y in CI: run axe.run(document) na openen modal en assert geen violations voor focus-trap en aria-modal. Gebruik onze WCAG checker/validator (https://wcagtool.nl/checker) als extra stap en installeer onze browser plugin (https://wcagtool.nl/plugin) voor snelle lokale scans.
Concrete testscript (puppeteer)
const puppeteer=require('puppeteer');(async()=>{const b=await puppeteer.launch();const p=await b.newPage();await p.goto('https://jouwsite.nl');await p.click('#openModal');await p.keyboard.press('Tab');const active=await p.evaluate(()=>document.activeElement.id);console.log(active);await b.close();})();
Gebruik onze checker na deze tests en deel resultaten via https://wcagtool.nl/contact (antwoord binnen 24 uur).
Laatste praktische tip
Gebruik deze compacte helper voor focusable elements in al je projecten — kopieer en plak direct:
function getFocusable(el){return Array.from(el.querySelectorAll('a[href],area[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),iframe,object,embed,[tabindex]:not([tabindex="-1"])')).filter(e=>!e.hasAttribute('disabled')&&e.getAttribute('tabindex')!=='-1');}
Test je implementatie meteen via onze WCAG checker: https://wcagtool.nl/checker. Download ook onze plugin voor in-browser checks: https://wcagtool.nl/plugin en stuur vragen via https://wcagtool.nl/contact — we reageren binnen 24 uur.