Il problema del focus nei componenti modali
I modal e gli overlay rappresentano una delle sfide più complesse per l’accessibilità web. Quando un modal si apre, gli screen reader e la navigazione da tastiera devono essere correttamente gestiti per evitare che l’utente possa navigare verso contenuti nascosti dietro l’overlay.
Secondo i dati del WebAIM Million 2025, il 38% dei siti analizzati presenta problemi critici nella gestione del focus nei componenti dinamici. In WordPress, dove plugin e temi spesso implementano modal personalizzati per lightbox, form di contatto, cookie banner e popup vari, il problema è ancora più diffuso.
Gli errori più comuni includono:
- Focus non spostato automaticamente all’apertura del modal
- Possibilità di navigare con Tab verso elementi sotto l’overlay
- Mancato ripristino del focus alla chiusura
- Assenza di attributi ARIA per comunicare lo stato agli assistive technology
- Impossibilità di chiudere il modal con Escape
WCAG 2.1 e i requisiti per i modal
Le Web Content Accessibility Guidelines 2.1 definiscono requisiti specifici per i componenti modali, principalmente sotto i criteri:
- 2.1.1 Keyboard (Livello A): tutte le funzionalità devono essere accessibili da tastiera
- 2.1.2 No Keyboard Trap (Livello A): il focus non deve rimanere intrappolato in modo permanente
- 2.4.3 Focus Order (Livello A): l’ordine di navigazione deve essere logico
- 4.1.2 Name, Role, Value (Livello A): i componenti devono comunicare il proprio ruolo
Per un modal, questo significa implementare un focus trap temporaneo che mantiene il focus all’interno del dialog fino alla chiusura, ma che può essere disattivato dall’utente.
Implementazione del focus trap in JavaScript vanilla
La gestione corretta del focus richiede una strategia precisa. Ecco un’implementazione completa e testata:
class ModalFocusManager {
constructor(modalElement) {
this.modal = modalElement;
this.focusableSelectors = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
this.previousActiveElement = null;
}
open() {
// Salva il focus corrente
this.previousActiveElement = document.activeElement;
// Ottieni elementi focalizzabili
this.focusableElements = this.modal.querySelectorAll(this.focusableSelectors);
this.firstFocusable = this.focusableElements[0];
this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
// Imposta ARIA attributes
this.modal.setAttribute('role', 'dialog');
this.modal.setAttribute('aria-modal', 'true');
// Nascondi contenuto esterno dagli screen reader
this.hideExternalContent();
// Sposta focus al primo elemento o al modal stesso
if (this.firstFocusable) {
this.firstFocusable.focus();
} else {
this.modal.setAttribute('tabindex', '-1');
this.modal.focus();
}
// Aggiungi event listeners
this.modal.addEventListener('keydown', this.handleKeyDown.bind(this));
document.addEventListener('focus', this.handleFocusEscape.bind(this), true);
}
handleKeyDown(e) {
// ESC chiude il modal
if (e.key === 'Escape') {
this.close();
return;
}
// TAB cycling
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === this.firstFocusable) {
e.preventDefault();
this.lastFocusable.focus();
}
} else {
if (document.activeElement === this.lastFocusable) {
e.preventDefault();
this.firstFocusable.focus();
}
}
}
}
handleFocusEscape(e) {
// Previeni focus su elementi esterni
if (!this.modal.contains(e.target)) {
e.stopPropagation();
this.firstFocusable.focus();
}
}
hideExternalContent() {
// Nascondi contenuto principale dagli screen reader
const mainContent = document.getElementById('main');
if (mainContent) {
mainContent.setAttribute('aria-hidden', 'true');
this.ariaHiddenElement = mainContent;
}
}
close() {
// Ripristina ARIA
if (this.ariaHiddenElement) {
this.ariaHiddenElement.removeAttribute('aria-hidden');
}
// Rimuovi listeners
this.modal.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('focus', this.handleFocusEscape, true);
// Ripristina focus
if (this.previousActiveElement && this.previousActiveElement.focus) {
this.previousActiveElement.focus();
}
}
}
Questa implementazione gestisce tutti gli aspetti critici: trap del focus con Tab cycling, chiusura con Escape, gestione degli screen reader e ripristino del focus originale.
Integrazione nei temi WordPress
Per integrare il focus management in un tema WordPress, è necessario enqueue dello script e inizializzare correttamente i modal:
// functions.php
function enqueue_modal_focus_manager() {
wp_enqueue_script(
'modal-focus-manager',
get_template_directory_uri() . '/js/modal-focus-manager.js',
array(),
'1.0.0',
true
);
}
add_action('wp_enqueue_scripts', 'enqueue_modal_focus_manager');
// Inizializzazione
document.addEventListener('DOMContentLoaded', function() {
const modalTriggers = document.querySelectorAll('[data-modal-trigger]');
modalTriggers.forEach(trigger => {
trigger.addEventListener('click', function(e) {
e.preventDefault();
const modalId = this.getAttribute('data-modal-trigger');
const modal = document.getElementById(modalId);
if (modal) {
const focusManager = new ModalFocusManager(modal);
modal.classList.add('is-open');
focusManager.open();
// Gestione chiusura
const closeButtons = modal.querySelectorAll('[data-modal-close]');
closeButtons.forEach(btn => {
btn.addEventListener('click', () => {
modal.classList.remove('is-open');
focusManager.close();
});
});
}
});
});
});
Gestione dei plugin WordPress comuni
Molti plugin WordPress popolari non implementano correttamente il focus management. Ecco come migliorare alcuni casi comuni:
Contact Form 7
Contact Form 7 non gestisce il focus nei suoi modal personalizzati. Per aggiungere la gestione:
document.addEventListener('wpcf7submit', function(event) {
const form = event.target;
const responseOutput = form.querySelector('.wpcf7-response-output');
if (responseOutput) {
responseOutput.setAttribute('tabindex', '-1');
responseOutput.setAttribute('role', 'alert');
responseOutput.focus();
}
}, false);
WooCommerce lightbox
Il lightbox delle immagini prodotto di WooCommerce necessita di focus trap. Puoi estendere la funzionalità esistente:
jQuery(document).on('click', '.woocommerce-product-gallery__trigger', function() {
setTimeout(() => {
const lightbox = document.querySelector('.pswp');
if (lightbox && lightbox.classList.contains('pswp--open')) {
const focusManager = new ModalFocusManager(lightbox);
focusManager.open();
}
}, 100);
});
Testing e validazione
La validazione del focus management richiede test manuali e automatizzati:
Test manuali essenziali
- Navigazione completa con solo tastiera (Tab, Shift+Tab, Enter, Escape)
- Test con screen reader (NVDA su Windows, VoiceOver su macOS)
- Verifica del focus visibile su ogni elemento interattivo
- Controllo del ripristino del focus alla chiusura
- Test su dispositivi touch con keyboard fisica
Strumenti automatizzati
- axe DevTools: identifica problemi di focus order e ARIA
- Lighthouse: audit automatico con Chrome DevTools
- Pa11y: testing automatizzato da CLI, integrabile in CI/CD
// Esempio pa11y per WordPress
const pa11y = require('pa11y');
async function testModal() {
const results = await pa11y('https://tuo-sito.test/pagina-con-modal', {
actions: [
'click element button[data-modal-trigger]',
'wait for element [role="dialog"] to be visible'
],
standard: 'WCAG2AA'
});
console.log(results);
}
testModal();
Performance e best practices
L’implementazione del focus management deve essere performante anche su siti con molti modal:
- Lazy initialization: inizializza ModalFocusManager solo quando necessario, non al caricamento pagina
- Event delegation: usa un singolo listener per tutti i trigger invece di listener individuali
- Debouncing: per modal che si aprono/chiudono rapidamente, implementa debouncing per evitare conflitti
- Cleanup: rimuovi sempre i listener quando il modal viene distrutto
Per siti gestiti con AgencyPilot, consigliamo di implementare questi pattern in un plugin custom condiviso tra tutti i clienti, permettendo aggiornamenti centralizzati quando emergono nuovi requisiti di accessibilità.
FAQ
Quale libreria JavaScript è meglio usare per il focus trap in WordPress?
Le librerie più affidabili sono focus-trap (7KB minified) e a11y-dialog (4KB). Entrambe sono mantenute attivamente e coprono tutti i casi edge. Per progetti custom, l’implementazione vanilla mostrata in questo articolo è sufficiente e evita dipendenze. Se usi già React/Vue, considera focus-trap-react o vue-focus-lock.
Come gestire il focus in modal annidati?
I modal annidati richiedono uno stack di focus manager. Quando si apre un secondo modal, il primo deve essere messo in pausa ma non chiuso. Salva i focus manager in un array: quando chiudi il modal corrente, riattiva il precedente nello stack. Assicurati che aria-modal="true" sia presente solo sul modal attivo in cima allo stack.
Il focus trap funziona con i page builder come Elementor?
Elementor genera markup complesso e usa il proprio sistema di popup. Per gestire correttamente il focus, aggancia il tuo script all’evento elementor/popup/show. Elementor Pro include già gestione base del focus dall’aggiornamento 3.11, ma manca il trap completo. Puoi estendere la funzionalità con un custom script che intercetta i popup e applica ModalFocusManager al container generato.
Come testare il focus management con automated testing?
Usa Playwright o Cypress con comandi di navigazione tastiera. In Playwright: await page.keyboard.press('Tab') e verifica con await expect(page.locator('button')).toBeFocused(). Per screen reader, Guidepup permette testing automatizzato con VoiceOver e NVDA. Integra questi test nella CI per prevenire regressioni quando aggiorni plugin o temi.
Devo implementare focus management anche per i cookie banner?
Assolutamente sì. I cookie banner sono modal a tutti gli effetti e devono seguire le stesse regole WCAG. Anzi, essendo spesso la prima interazione dell’utente, sono ancora più critici. Sposta il focus al banner all’apertura, implementa keyboard navigation tra i pulsanti, e permetti chiusura con Escape se l’accettazione non è obbligatoria per legge. Plugin come Complianz hanno migliorato l’accessibilità nelle versioni recenti, ma verifica sempre manualmente.