Pourquoi utiliser l’élément dialog ?
Pendant longtemps, créer une modale web correcte signifiait assembler une couche sombre, un panneau positionné en fixed, un gestionnaire d’échappement clavier, un piège de focus, des attributs ARIA, puis beaucoup de tests pour vérifier que les lecteurs d’écran ne se perdaient pas dans la page.
L’élément HTML dialog ne supprime pas toute la complexité, mais il fournit une base native beaucoup plus solide. Il sait représenter une boîte de dialogue, peut être ouvert en mode modal, gère une partie du comportement clavier et expose une API JavaScript simple : show(), showModal() et close().
Ce sujet complète bien les approches de progressive enhancement, car une modale ne devrait jamais être le seul chemin possible pour accéder à une information ou valider une action importante.
Une première modale native
Voici un exemple minimal :
<button type="button" id="open-delete-dialog">
Supprimer le fichier
</button>
<dialog id="delete-dialog" aria-labelledby="delete-title">
<h2 id="delete-title">Confirmer la suppression</h2>
<p>Cette action est définitive. Le fichier ne pourra pas être restauré.</p>
<form method="dialog">
<button value="cancel">Annuler</button>
<button value="confirm">Supprimer</button>
</form>
</dialog>
const openButton = document.querySelector<HTMLButtonElement>("#open-delete-dialog");
const dialog = document.querySelector<HTMLDialogElement>("#delete-dialog");
if (!openButton || !dialog) {
throw new Error("Éléments introuvables");
}
openButton.addEventListener("click", () => {
dialog.showModal();
});
dialog.addEventListener("close", () => {
if (dialog.returnValue === "confirm") {
console.log("Suppression confirmée");
}
});
Le point important est showModal(). Contrairement à show(), cette méthode ouvre une vraie boîte de dialogue modale : le reste de la page devient inerte pour l’utilisateur. Le navigateur place l’élément dans le top layer, au-dessus du reste de l’interface.
Le formulaire avec method="dialog" est également spécifique : il permet de fermer automatiquement la boîte de dialogue et de renseigner dialog.returnValue avec la valeur du bouton utilisé.
Dialog non modal ou modal ?
dialog peut être utilisé de deux façons.
Avec show(), la boîte est affichée sans bloquer le reste de la page. C’est utile pour un panneau temporaire, une aide contextuelle ou une petite interaction qui ne doit pas interrompre l’utilisateur.
Avec showModal(), l’utilisateur doit traiter la boîte de dialogue avant de revenir au reste de l’interface. C’est adapté à une confirmation destructrice, une authentification, une édition courte ou une décision explicite.
La règle pratique est simple : si l’utilisateur peut continuer son action sans répondre immédiatement, évitez la modale. Une interface trop modale fatigue rapidement, notamment au clavier et sur mobile.
Pour les bulles contextuelles, menus flottants et panneaux légers, la Popover API est souvent plus appropriée que dialog.
Style CSS de base
L’élément dialog possède un rendu par défaut, mais il est rarement suffisant pour une interface de production. On peut le styliser comme un composant classique.
dialog {
width: min(32rem, calc(100vw - 2rem));
border: 0;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 0.25);
}
dialog::backdrop {
background: rgb(0 0 0 / 0.45);
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
Le pseudo-élément ::backdrop cible l’arrière-plan généré par le navigateur lorsqu’une boîte de dialogue modale est ouverte. Il évite d’ajouter manuellement un élément div uniquement pour créer un voile sombre.
Dans une architecture CSS plus large, vous pouvez intégrer cette règle dans une couche dédiée aux composants, comme expliqué dans l’article sur les CSS cascade layers, afin d’éviter les conflits de priorité.
Accessibilité : ne pas tout déléguer au navigateur
dialog apporte une meilleure sémantique qu’une simple div, mais cela ne suffit pas. Une modale accessible doit avoir un nom clair, une action de fermeture évidente et un comportement clavier prévisible.
Le nom accessible peut être fourni avec aria-labelledby, en pointant vers le titre visible de la boîte :
<dialog id="profile-dialog" aria-labelledby="profile-dialog-title">
<h2 id="profile-dialog-title">Modifier le profil</h2>
<form method="dialog">
<label>
Nom affiché
<input name="displayName" autocomplete="name" />
</label>
<div class="dialog-actions">
<button value="cancel">Annuler</button>
<button value="save">Enregistrer</button>
</div>
</form>
</dialog>
Il faut aussi penser au focus initial. Sur une modale de confirmation destructive, placer automatiquement le focus sur le bouton dangereux peut provoquer des validations accidentelles. Il est souvent préférable de placer le focus sur le titre, le bouton d’annulation ou le premier champ pertinent.
<h2 id="delete-title" tabindex="-1">Confirmer la suppression</h2>
const title = document.querySelector<HTMLElement>("#delete-title");
dialog.addEventListener("close", () => {
openButton.focus();
});
openButton.addEventListener("click", () => {
dialog.showModal();
title?.focus();
});
Restaurer le focus sur le bouton d’ouverture après fermeture est une bonne pratique : l’utilisateur revient à l’endroit logique de son parcours.
Ces principes rejoignent ceux des formulaires accessibles : labels explicites, erreurs compréhensibles, ordre de tabulation cohérent et feedback clair.
Gérer la fermeture sans mauvaise surprise
Une boîte de dialogue modale peut être fermée avec la touche Échap. C’est généralement souhaitable, mais certaines actions nécessitent de contrôler ce comportement.
dialog.addEventListener("cancel", (event) => {
const hasUnsavedChanges = true;
if (hasUnsavedChanges) {
event.preventDefault();
console.log("Demander une confirmation avant fermeture");
}
});
L’événement cancel est déclenché notamment lors d’une tentative de fermeture avec Échap. En appelant preventDefault(), on peut empêcher la fermeture automatique.
À utiliser avec retenue : bloquer la touche Échap peut être frustrant. Si vous le faites, fournissez une alternative claire, comme une confirmation interne ou un bouton de fermeture visible.
Intégration dans Vue
Avec Vue, il est souvent plus propre de contrôler l’ouverture via une référence DOM plutôt que de rendre conditionnellement l’élément à chaque changement d’état.
<script setup lang="ts">
import { ref } from "vue";
const dialogRef = ref<HTMLDialogElement | null>(null);
function openDialog() {
dialogRef.value?.showModal();
}
function handleClose() {
console.log("Valeur :", dialogRef.value?.returnValue);
}
</script>
<template>
<button type="button" @click="openDialog">
Ouvrir les préférences
</button>
<dialog ref="dialogRef" aria-labelledby="preferences-title" @close="handleClose">
<h2 id="preferences-title">Préférences</h2>
<form method="dialog">
<label>
Thème
<select name="theme">
<option value="system">Système</option>
<option value="light">Clair</option>
<option value="dark">Sombre</option>
</select>
</label>
<button value="save">Enregistrer</button>
</form>
</dialog>
</template>
Cette approche garde le comportement natif du formulaire tout en laissant Vue gérer la logique applicative.
Intégration dans React
En React, le principe est similaire : l’élément dialog reste dans le DOM, et l’API native est pilotée via une ref.
import { useRef } from "react";
export function SettingsDialog() {
const dialogRef = useRef<HTMLDialogElement | null>(null);
return (
<>
<button type="button" onClick={() => dialogRef.current?.showModal()}>
Ouvrir les paramètres
</button>
<dialog
ref={dialogRef}
aria-labelledby="settings-title"
onClose={() => console.log(dialogRef.current?.returnValue)}
>
<h2 id="settings-title">Paramètres</h2>
<form method="dialog">
<label>
Densité d’affichage
<select name="density">
<option value="comfortable">Confortable</option>
<option value="compact">Compacte</option>
</select>
</label>
<button value="cancel">Annuler</button>
<button value="save">Enregistrer</button>
</form>
</dialog>
</>
);
}
Il faut éviter de synchroniser naïvement une prop open avec l’attribut HTML open. Pour une modale, showModal() déclenche un comportement spécifique que le simple attribut ne reproduit pas entièrement.
Prévoir une amélioration progressive
Une modale ne doit pas rendre une fonctionnalité inaccessible si JavaScript échoue. Pour une action critique, prévoyez une page ou un formulaire classique en fallback.
<a href="/account/delete">Supprimer le compte</a>
Puis, lorsque JavaScript est disponible, vous pouvez intercepter le clic pour ouvrir la boîte de dialogue native. Le lien reste fonctionnel sans script.
const link = document.querySelector<HTMLAnchorElement>("a[href='/account/delete']");
link?.addEventListener("click", (event) => {
if (typeof HTMLDialogElement === "undefined") {
return;
}
event.preventDefault();
dialog.showModal();
});
Cette logique s’accorde avec les bases du progressive enhancement : commencer par une expérience robuste, puis ajouter l’interaction avancée.
Erreurs fréquentes
La première erreur consiste à utiliser dialog pour tout élément flottant. Une liste d’actions, un tooltip ou un menu utilisateur n’est pas forcément une boîte de dialogue. Dans ces cas, Popover ou une navigation classique seront souvent plus adaptés.
La deuxième erreur consiste à oublier le titre accessible. Une modale sans nom oblige les utilisateurs de technologies d’assistance à deviner son rôle.
La troisième erreur est de cacher le bouton de fermeture. Même si Échap fonctionne, un bouton visible reste nécessaire pour les utilisateurs qui ne connaissent pas ou ne peuvent pas utiliser ce raccourci.
La quatrième erreur est d’interrompre l’utilisateur pour des informations non critiques. Une notification non bloquante, éventuellement annoncée avec une ARIA live region, est souvent préférable.
Conclusion
L’élément dialog est un bon exemple de plateforme web moderne : il remplace une partie du JavaScript fragile par une primitive native, tout en laissant assez de contrôle pour construire des interfaces professionnelles.
Pour l’utiliser correctement, retenez trois règles : ouvrez les vraies modales avec showModal(), donnez toujours un nom accessible à la boîte de dialogue, et restaurez le focus après fermeture. Le résultat sera plus robuste, plus maintenable et plus respectueux des usages clavier et lecteurs d’écran.