// html · Web Platform Advent #18
L’elemento HTML dialog: vere finestre modali senza librerie
L’elemento HTML dialog ti offre una vera finestra modale (livello superiore, pagina inerte, backdrop e gestione di Esc) con del semplice markup. Ecco showModal contro show, form method=dialog, returnValue, l’attributo closedby e l’abitudine da evitare.
Ogni sviluppatore front-end ha costruito una finestra modale a mano: un div in posizione fissa, una guerra di z-index, un listener per il clic esterno, un gestore di Esc e un po’ di acrobazie sul focus perché la tastiera non finisca a vagare dietro l’overlay. L’elemento <dialog> sostituisce tutto questo con un tag e una chiamata di metodo. Ecco che cosa fa davvero, e i due o tre dettagli che decidono se la tua finestra di dialogo si comporta correttamente.
La finestra di dialogo funzionante più piccola
Una finestra di dialogo è markup inerte finché non la apri da JavaScript:
<dialog id="confirm">
<p>Eliminare questo file?</p>
<button autofocus>Annulla</button>
</dialog>
<button id="open">Elimina</button> const dialog = document.getElementById("confirm");
document.getElementById("open")
.addEventListener("click", () => dialog.showModal()); Quell’unica chiamata a showModal() fa molto più che rendere visibile un elemento.
showModal() contro show(): la differenza che conta
Questi due metodi non sono varianti dello stesso tema. Producono risultati davvero diversi.
showModal() apre una finestra di dialogo modale. Il browser la colloca nel livello superiore, quindi viene disegnata sopra l’intera pagina senza alcun z-index in gioco. Disegna uno pseudo-elemento ::backdrop dietro di essa. Rende tutto il resto inerte, il che significa che nel resto del documento non si può cliccare né entrare con il tasto Tab. E imposta implicitamente aria-modal="true".
show() apre una finestra di dialogo non modale. La pagina resta interattiva, non c’è livello superiore, non c’è backdrop, e aria-modal è false.
L’inerzia è l’aspetto che si tende a sottovalutare. Rendere lo sfondo non cliccabile è facile; renderlo non raggiungibile con il tasto Tab è la parte che le finestre modali fatte a mano sbagliano quasi sempre, e qui è il browser a occuparsene per te.
L’abitudine da abbandonare: l’attributo open
Un elemento <dialog> ha anche un attributo booleano open, e ricorrere a quello è l’errore più comune con questo elemento.
<!-- funziona, ma questa NON è una finestra modale -->
<dialog open>Sono non modale, qualunque cosa ti aspettassi.</dialog> Una finestra di dialogo mostrata tramite l’attributo open è sempre non modale. Nessun livello superiore, nessun backdrop, nessuna pagina inerte. MDN consiglia di usare show() o showModal() invece dell’attributo, e descrive l’attivazione manuale di open come una pratica non consigliata. Se la tua finestra modale sembra corretta ma la pagina dietro risponde ancora ai clic, quasi certamente il motivo è questo.
form method="dialog": la funzionalità senza equivalenti
Questa è la parte di <dialog> che nient’altro replica, ed è il motivo per cui una finestra di dialogo di conferma può non richiedere JavaScript oltre a quello che serve per aprirla.
<dialog id="confirm">
<form method="dialog">
<p>Eliminare questo file?</p>
<button value="cancel" autofocus>Annulla</button>
<button value="delete">Elimina</button>
</form>
</dialog> Quando un form all’interno di una finestra di dialogo viene inviato con method="dialog", la finestra si chiude invece di mandare una richiesta. Gli stati dei controlli del form vengono salvati ma non inviati, e il returnValue della finestra di dialogo viene impostato sul valore del pulsante che è stato attivato.
Quindi leggi la risposta in un secondo momento:
dialog.addEventListener("close", () => {
if (dialog.returnValue === "delete") {
// l’utente ha confermato
}
}); Due pulsanti, un attributo, e sai quale è stato premuto. Nessun gestore di clic, nessun wrapper con promise, nessuna variabile di stato.
La trappola del campo obbligatorio
C’è un comportamento che sorprende la prima volta e che vale la pena conoscere prima che ti costi un pomeriggio.
Se la finestra di dialogo contiene un form con input required, l’invio esegue comunque la validazione. Un semplice pulsante non può chiudere la finestra finché quei campi non sono validi, il che significa che il tuo pulsante Annulla smette di funzionare proprio quando l’utente ne ha più bisogno. Le soluzioni documentate sono aggiungere formnovalidate al pulsante di chiusura, oppure chiamare dialog.close() dal tuo codice:
<button value="cancel" formnovalidate>Annulla</button> Esc e l’attributo closedby
La chiusura da tastiera non è uniforme, e la differenza segue la distinzione tra modale e non modale.
Una finestra di dialogo aperta con showModal() può essere chiusa con Esc per impostazione predefinita. Una finestra di dialogo non modale non si chiude con Esc per impostazione predefinita.
L’attributo closedby rende tutto questo esplicito invece che implicito:
<dialog closedby="any"> <!-- Esc, clic esterno o il tuo codice -->
<dialog closedby="closerequest"> <!-- Esc o il tuo codice -->
<dialog closedby="none"> <!-- solo il tuo codice --> closedby="any" è ciò che ti dà la chiusura leggera, cioè il comportamento di clic esterno che di solito si scrive a mano. closedby="none" serve per quelle rare finestre di dialogo che non devono essere chiuse per errore, e comporta un obbligo: devi fornire una via d’uscita visibile.
Curare bene l’accessibilità
L’elemento ti offre gran parte del lavoro già fatto, ma tre punti restano a carico tuo.
Usa autofocus in modo consapevole. Con showModal(), il focus arriva sul primo elemento focalizzabile annidato. Spesso non è quello che vuoi. Metti autofocus sull’elemento con cui l’utente deve interagire subito, oppure sul pulsante di chiusura se non c’è nulla di più urgente.
Fornisci sempre un pulsante di chiusura. Il modo più solido per garantire che ogni utente possa uscire dalla finestra di dialogo è un pulsante esplicito, non soltanto Esc o un clic esterno.
Non mettere tabindex sull’elemento dialog stesso. Non è interattivo e non riceve il focus. Sono i suoi contenuti a riceverlo.
Applicare stili al backdrop
Il livello oscurato dietro una finestra modale è un vero pseudo-elemento a cui puoi applicare degli stili:
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
backdrop-filter: blur(2px);
} Esiste solo per le finestre di dialogo aperte con showModal(), il che è un altro modo per verificare a colpo d’occhio se sei davvero in modalità modale.
dialog o popover?
Entrambi vengono disegnati nel livello superiore, quindi la domanda torna di continuo. La distinzione riguarda l’interruzione.
Usa <dialog> con showModal() quando l’utente deve occuparsi di qualcosa prima di proseguire: una conferma, una scelta obbligatoria, un form che blocca il flusso. Bloccare la pagina è esattamente il punto.
Usa la Popover API per la UI sovrapposta e transitoria che non deve interrompere: menu, tooltip, schede, notifiche. Lì la pagina deve restare utilizzabile.
Un test utile: se sarebbe sbagliato che l’utente lo ignorasse e tirasse dritto, è una finestra di dialogo. Se ignorarlo è una risposta perfettamente ragionevole, è un popover.
In sintesi
L’elemento <dialog> trasforma un pezzo di UI davvero difficile in semplice markup. Aprilo con showModal() e ottieni il livello superiore, un backdrop, una pagina inerte e la gestione di Esc senza scriverne una riga. Usa form method="dialog" e returnValue e ottieni la risposta dell’utente senza un solo gestore di clic. Evita l’attributo open, ricordati formnovalidate sul pulsante di annullamento, e posiziona tu stesso autofocus.
Quello che resta è una finestra di dialogo di conferma in una quindicina di righe di HTML che si comporta correttamente per chi usa la tastiera e per chi usa uno screen reader, il che è più di quanto facciano la maggior parte di quelle costruite a mano che sostituisce.
Domande frequenti
- Qual è la differenza tra show() e showModal()?
- showModal() apre una finestra di dialogo modale: il browser la colloca nel livello superiore, disegna un ::backdrop dietro di essa, rende inerte il resto della pagina in modo che nulla all’esterno possa essere cliccato o raggiunto con il tasto Tab, e imposta implicitamente aria-modal="true". show() apre una finestra di dialogo non modale: il resto della pagina resta interattivo, non ci sono né livello superiore né backdrop, e aria-modal è false. Se vuoi una vera finestra modale, showModal() è il metodo giusto.
- Devo aprire una finestra di dialogo con l’attributo open?
- No. MDN afferma chiaramente che è consigliabile usare il metodo show() o showModal() invece dell’attributo open. Una finestra di dialogo mostrata tramite l’attributo open è sempre non modale, quindi non ottieni né il livello superiore, né il backdrop, né l’inerzia che probabilmente volevi. Attivare e disattivare l’attributo a mano è esplicitamente descritto come una pratica non consigliata.
- Che cosa fa form method="dialog"?
- Un form all’interno di una finestra di dialogo con method="dialog" chiude la finestra quando viene inviato, invece di mandare una richiesta. Gli stati dei controlli del form vengono salvati ma non inviati, e la proprietà returnValue della finestra di dialogo viene impostata sul valore del pulsante che è stato attivato. Così sai quale pulsante ha premuto l’utente senza scrivere una riga di JavaScript.
- Il tasto Esc chiude una finestra di dialogo?
- Per una finestra modale aperta con showModal(), sì: può essere chiusa con Esc per impostazione predefinita. Per una finestra di dialogo non modale, no, Esc non la chiude per impostazione predefinita. L’attributo closedby ti permette di cambiare questo comportamento: closerequest consente Esc, any consente anche un clic esterno, e none significa che solo il tuo codice può chiuderla.
- Perché la mia finestra di dialogo non si chiude quando il form ha un campo obbligatorio?
- Perché il form esegue comunque la validazione. Se la finestra di dialogo contiene input required, un semplice pulsante di invio non può chiuderla finché quei campi non sono validi. Aggiungi formnovalidate al pulsante destinato a chiudere la finestra, oppure chiama dialog.close() dal tuo codice.