// html · Web Platform Advent #18
L'élément HTML dialog : de vraies modales sans bibliothèque
L'élément HTML dialog vous donne une vraie modale (couche supérieure, page inerte, arrière-plan et gestion d'Échap) à partir d'un simple balisage. Voici showModal contre show, form method=dialog, returnValue, l'attribut closedby et la seule habitude à abandonner.
Chaque développeur front-end a déjà construit une modale à la main : un div en position fixe, une guerre de z-index, un écouteur de clic à l'extérieur, un gestionnaire d'Échap, et quelques acrobaties de focus pour que le clavier ne parte pas se promener derrière la surcouche. L'élément <dialog> remplace tout cela par une balise et un appel de méthode. Voici ce qu'il fait réellement, et les deux ou trois détails qui décident si votre boîte de dialogue se comporte correctement.
La plus petite boîte de dialogue fonctionnelle
Une boîte de dialogue n'est qu'un balisage inerte tant que vous ne l'ouvrez pas depuis JavaScript :
<dialog id="confirm">
<p>Supprimer ce fichier ?</p>
<button autofocus>Annuler</button>
</dialog>
<button id="open">Supprimer</button> const dialog = document.getElementById("confirm");
document.getElementById("open")
.addEventListener("click", () => dialog.showModal()); Ce simple appel à showModal() fait considérablement plus que rendre un élément visible.
showModal() contre show() : la différence qui compte
Ces deux méthodes ne sont pas des variations sur un même thème. Elles produisent des résultats réellement différents.
showModal() ouvre une boîte de dialogue modale. Le navigateur la place dans la couche supérieure, elle s'affiche donc au-dessus de toute la page sans qu'aucun z-index n'intervienne. Il affiche un pseudo-élément ::backdrop derrière elle. Il rend tout le reste inerte, ce qui signifie que le reste du document ne peut être ni cliqué ni atteint au clavier. Et il définit implicitement aria-modal="true".
show() ouvre une boîte de dialogue non modale. La page reste interactive, il n'y a pas de couche supérieure, pas d'arrière-plan, et aria-modal vaut false.
L'inertie est la partie que l'on sous-estime. Rendre l'arrière-plan non cliquable est facile ; le rendre inaccessible à la tabulation est ce que les modales faites main ratent presque toujours, et ici c'est le navigateur qui s'en charge pour vous.
L'habitude à abandonner : l'attribut open
Un <dialog> possède aussi un attribut booléen open, et y recourir est l'erreur la plus fréquente avec cet élément.
<!-- ça marche, mais ce n'est PAS une modale -->
<dialog open>Je suis non modale, quoi que vous ayez attendu.</dialog> Une boîte de dialogue affichée via l'attribut open est toujours non modale. Pas de couche supérieure, pas d'arrière-plan, pas de page inerte. MDN recommande d'utiliser show() ou showModal() plutôt que l'attribut, et décrit le basculement manuel de open comme n'étant pas la pratique recommandée. Si votre modale a l'air correcte mais que la page derrière elle répond encore aux clics, c'est presque certainement la raison.
form method="dialog" : la fonctionnalité sans équivalent
C'est la partie de <dialog> que rien d'autre ne reproduit, et c'est la raison pour laquelle une boîte de dialogue de confirmation peut se passer de tout JavaScript au-delà de son ouverture.
<dialog id="confirm">
<form method="dialog">
<p>Supprimer ce fichier ?</p>
<button value="cancel" autofocus>Annuler</button>
<button value="delete">Supprimer</button>
</form>
</dialog> Quand un formulaire situé dans une boîte de dialogue est soumis avec method="dialog", la boîte de dialogue se ferme au lieu d'envoyer une requête. L'état des contrôles du formulaire est enregistré mais pas soumis, et la propriété returnValue de la boîte de dialogue reçoit la valeur du bouton qui a été activé.
Vous lisez donc la réponse ensuite :
dialog.addEventListener("close", () => {
if (dialog.returnValue === "delete") {
// l'utilisateur a confirmé
}
}); Deux boutons, un attribut, et vous savez lequel a été pressé. Pas de gestionnaires de clic, pas d'enrobage en promesse, pas de variable d'état.
Le piège du champ requis
Un comportement surprend tout le monde la première fois et mérite d'être connu avant qu'il ne vous coûte un après-midi.
Si la boîte de dialogue contient un formulaire avec des champs required, la soumission déclenche quand même la validation. Un simple bouton ne peut pas fermer la boîte de dialogue tant que ces champs sont invalides, ce qui veut dire que votre bouton Annuler cesse de fonctionner précisément quand l'utilisateur en a le plus besoin. Les réponses documentées consistent à ajouter formnovalidate au bouton de fermeture, ou à appeler dialog.close() depuis votre propre code :
<button value="cancel" formnovalidate>Annuler</button> Échap, et l'attribut closedby
La fermeture au clavier n'est pas uniforme, et la différence suit la séparation modale / non modale.
Une boîte de dialogue ouverte avec showModal() peut être fermée avec Échap par défaut. Une boîte de dialogue non modale ne se ferme pas avec Échap par défaut.
L'attribut closedby rend cela explicite plutôt qu'implicite :
<dialog closedby="any"> <!-- Échap, clic à l'extérieur, ou votre code -->
<dialog closedby="closerequest"> <!-- Échap ou votre code -->
<dialog closedby="none"> <!-- votre code uniquement --> closedby="any" est ce qui vous donne la fermeture légère, ce comportement de clic à l'extérieur que l'on écrit habituellement à la main. closedby="none" est réservé à la rare boîte de dialogue qui ne doit pas être fermée accidentellement, et il s'accompagne d'une obligation : vous devez fournir une sortie visible.
Réussir l'accessibilité
L'élément vous offre l'essentiel, mais trois points restent à votre charge.
Utilisez autofocus délibérément. Avec showModal(), le focus se pose sur le premier élément focalisable imbriqué. Ce n'est souvent pas celui que vous voulez. Placez autofocus sur l'élément avec lequel l'utilisateur doit interagir immédiatement, ou sur le bouton de fermeture si rien d'autre n'est plus urgent.
Fournissez toujours un bouton de fermeture. Le moyen le plus robuste de garantir que chaque utilisateur puisse quitter la boîte de dialogue est un bouton explicite, pas seulement Échap ou un clic à l'extérieur.
Ne mettez pas de tabindex sur la boîte de dialogue elle-même. Elle n'est pas interactive et ne reçoit pas le focus. Ce sont ses contenus qui le reçoivent.
Styler l'arrière-plan
La couche assombrie derrière une modale est un véritable pseudo-élément que vous pouvez styler :
dialog::backdrop {
background: rgb(0 0 0 / 0.5);
backdrop-filter: blur(2px);
} Il n'existe que pour les boîtes de dialogue ouvertes avec showModal(), ce qui est une autre façon de vérifier d'un coup d'œil si vous êtes réellement en mode modal.
dialog ou popover ?
Les deux s'affichent dans la couche supérieure, la question revient donc sans arrêt. La distinction porte sur l'interruption.
Utilisez <dialog> avec showModal() quand l'utilisateur doit traiter quelque chose avant de continuer : une confirmation, un choix obligatoire, un formulaire qui bloque le parcours. Bloquer la page est tout l'intérêt.
Utilisez l'API Popover pour les surcouches d'interface passagères qui ne doivent pas interrompre : menus, infobulles, cartes, notifications. Là, la page doit rester utilisable.
Un test utile : s'il serait fâcheux que l'utilisateur l'ignore et poursuive, c'est une boîte de dialogue. Si l'ignorer est une réaction parfaitement raisonnable, c'est un popover.
En résumé
L'élément <dialog> transforme un morceau d'interface réellement difficile en simple balisage. Ouvrez-le avec showModal() et vous obtenez la couche supérieure, un arrière-plan, une page inerte et la gestion d'Échap sans en écrire une ligne. Utilisez form method="dialog" et returnValue et vous obtenez la réponse de l'utilisateur sans un seul gestionnaire de clic. Évitez l'attribut open, pensez à formnovalidate sur votre bouton d'annulation, et placez autofocus vous-même.
Il reste une boîte de dialogue de confirmation en une quinzaine de lignes de HTML qui se comporte correctement pour les utilisateurs de clavier et de lecteur d'écran, ce qui est plus que la plupart de celles faites main qu'elle remplace.
Questions fréquentes
- Quelle est la différence entre show() et showModal() ?
- showModal() ouvre une boîte de dialogue modale : le navigateur la place dans la couche supérieure, affiche un ::backdrop derrière elle, rend le reste de la page inerte pour que rien à l'extérieur ne puisse être cliqué ni atteint au clavier, et définit implicitement aria-modal="true". show() ouvre une boîte de dialogue non modale : le reste de la page reste interactif, il n'y a ni couche supérieure ni arrière-plan, et aria-modal vaut false. Si vous voulez une vraie modale, c'est showModal() qu'il faut utiliser.
- Faut-il ouvrir une boîte de dialogue avec l'attribut open ?
- Non. MDN indique clairement qu'il est recommandé d'utiliser la méthode show() ou showModal() plutôt que l'attribut open. Une boîte de dialogue affichée via l'attribut open est toujours non modale : vous n'obtenez donc ni la couche supérieure, ni l'arrière-plan, ni l'inertie que vous attendiez probablement. Basculer l'attribut à la main est explicitement décrit comme n'étant pas la pratique recommandée.
- À quoi sert form method="dialog" ?
- Un formulaire placé dans une boîte de dialogue avec method="dialog" ferme celle-ci lors de la soumission au lieu d'envoyer une requête. L'état des contrôles du formulaire est enregistré mais pas soumis, et la propriété returnValue de la boîte de dialogue reçoit la valeur du bouton qui a été activé. Vous savez ainsi quel bouton l'utilisateur a pressé, sans la moindre ligne de JavaScript.
- La touche Échap ferme-t-elle une boîte de dialogue ?
- Pour une modale ouverte avec showModal(), oui : elle peut être fermée avec Échap par défaut. Pour une boîte de dialogue non modale, non, Échap ne la ferme pas par défaut. L'attribut closedby permet de changer cela : closerequest autorise Échap, any autorise en plus un clic à l'extérieur, et none signifie que seul votre propre code peut la fermer.
- Pourquoi ma boîte de dialogue ne se ferme-t-elle pas quand le formulaire contient un champ requis ?
- Parce que le formulaire exécute quand même la validation. Si la boîte de dialogue contient des champs required, un simple bouton de soumission ne peut pas la fermer tant que ces champs sont invalides. Ajoutez formnovalidate au bouton censé fermer la boîte de dialogue, ou appelez dialog.close() depuis votre propre code à la place.