Ouvrir une boîte de dialogue, confirmer une suppression, afficher un formulaire sans quitter la page… les popups JavaScript font partie du quotidien de tout développeur web. Pourtant, entre les méthodes natives du navigateur, les modales construites à la main et la balise <dialog> HTML5, les approches ne manquent pas, et toutes ne se valent pas.
Ce guide couvre l’intégralité du sujet : des trois popups natifs hérités des années 2000 jusqu’à la méthode moderne recommandée en 2026, en passant par le code complet d’une fenêtre modale personnalisée, les règles d’accessibilité, les déclencheurs intelligents et les contraintes SEO à respecter sur mobile. Que vous partiez de zéro ou que vous souhaitiez moderniser un code existant, vous trouverez ici tout ce dont vous avez besoin.
Les 3 popups natifs du navigateur : alert, confirm et prompt
Avant d’écrire la moindre ligne de CSS, JavaScript propose trois méthodes intégrées pour afficher des fenêtres contextuelles. Elles sont simples, universellement supportées et ne nécessitent aucune dépendance.
`window.alert()` affiche un message et un bouton « OK ». L’exécution du script est bloquée jusqu’à ce que l’utilisateur clique.
// Affiche un message simple
window.alert("Votre fichier a bien été enregistré.");
`window.confirm()` affiche un message avec deux boutons (« OK » et « Annuler ») et retourne un booléen.
// Demande une confirmation avant suppression
const confirmation = window.confirm("Supprimer cet élément définitivement ?");
if (confirmation) {
supprimerElement();
}
`window.prompt()` affiche un champ de saisie et retourne la valeur entrée par l’utilisateur, ou null si l’utilisateur annule.
// Demande une valeur à l'utilisateur
const prenom = window.prompt("Quel est votre prénom ?", "Jean");
if (prenom !== null) {
console.log("Bonjour, " + prenom);
}
Pourquoi ces méthodes ne s’utilisent plus en production
Ces trois popups natifs souffrent de limitations rédhibitoires pour tout projet professionnel :
alert est ouverte, tout le JavaScript de la page est suspendu. Cela peut poser des problèmes avec les timers, les WebSockets ou les animations.Ces méthodes restent utiles pour le débogage rapide ou les scripts d’automatisation simples. Pour tout ce qui touche à l’interface utilisateur en production, il faut construire une vraie fenêtre modale.
Créer une fenêtre modale personnalisée avec HTML, CSS et JavaScript
Une fenêtre modale personnalisée (aussi appelée popup HTML ou modal JavaScript) se compose de deux éléments : un overlay (le fond semi-transparent qui recouvre la page) et la boîte de dialogue elle-même. Voici le code complet, commenté ligne par ligne.
Structure HTML
<!-- Bouton qui déclenche l'ouverture de la modale -->
<button id="btn-ouvrir" type="button">Ouvrir la modale</button>
<!-- Overlay : fond semi-transparent qui recouvre la page -->
<div id="overlay" aria-hidden="true">
<!-- Boîte de dialogue : le contenu visible de la modale -->
<div
id="ma-modale"
class="modale"
role="dialog"
aria-modal="true"
aria-labelledby="modale-titre"
aria-describedby="modale-description"
>
<!-- En-tête avec titre et bouton de fermeture -->
<header>
<h2 id="modale-titre">Confirmer l'action</h2>
<button
id="btn-fermer"
class="btn-fermer"
type="button"
aria-label="Fermer la fenêtre modale"
>
<!-- Icône SVG de croix, masquée aux lecteurs d'écran -->
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
<path d="M12 4L4 12M4 4l8 8" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
</header>
<!-- Corps de la modale -->
<div>
<p id="modale-description">
Êtes-vous sûr de vouloir effectuer cette action ? Elle est irréversible.
</p>
</div>
<!-- Pied de page avec les boutons d'action -->
<footer>
<button id="btn-annuler" type="button">Annuler</button>
<button id="btn-confirmer" type="button">Confirmer</button>
</footer>
</div>
</div>
Styles CSS
/* Overlay : fond semi-transparent, centrage de la modale */
.overlay {
position: fixed;
inset: 0; /* équivalent à top/right/bottom/left: 0 */
background-color: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
/* Masquée par défaut */
visibility: hidden;
opacity: 0;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
/* État ouvert de l'overlay */
.overlay.est-ouverte {
visibility: visible;
opacity: 1;
}
/* Boîte de dialogue */
.modale {
background: #ffffff;
border-radius: 8px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
width: min(520px, calc(100vw - 2rem));
max-height: calc(100vh - 2rem);
overflow-y: auto;
padding: 0;
/* Animation d'entrée */
transform: translateY(-16px);
transition: transform 0.2s ease;
}
.overlay.est-ouverte .modale {
transform: translateY(0);
}
/* En-tête */
.modale-entete {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.modale-entete h2 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
}
/* Bouton de fermeture */
.btn-fermer {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
color: #6b7280;
/* Zone tactile minimale recommandée : 44x44px */
min-width: 44px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-fermer:hover { background: #f3f4f6; }
.btn-fermer:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
/* Corps */
.modale-corps { padding: 1.5rem; }
.modale-corps p { margin: 0; color: #374151; line-height: 1.6; }
/* Pied de page */
.modale-pied {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-top: 1px solid #e5e7eb;
}
/* Boutons */
.btn-principal {
background: #3b82f6;
color: #fff;
border: none;
border-radius: 6px;
padding: 0.5rem 1.25rem;
font-size: 0.9375rem;
cursor: pointer;
min-height: 44px;
}
.btn-principal:hover { background: #2563eb; }
.btn-secondaire {
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 0.5rem 1.25rem;
font-size: 0.9375rem;
cursor: pointer;
min-height: 44px;
}
.btn-secondaire:hover { background: #e5e7eb; }
/* Respect des préférences de mouvement réduit */
@media (prefers-reduced-motion: reduce) {
.overlay, .modale { transition: none; }
}
JavaScript : ouverture, fermeture, clic extérieur et touche Échap
// Récupération des éléments du DOM
const overlay = document.getElementById('overlay');
const btnOuvrir = document.getElementById('btn-ouvrir');
const btnFermer = document.getElementById('btn-fermer');
const btnAnnuler = document.getElementById('btn-annuler');
// Mémorise l'élément qui a ouvert la modale
// pour y restituer le focus à la fermeture
let declencheur = null;
/**
* Ouvre la modale et déplace le focus à l'intérieur.
* @param {HTMLElement} el - L'élément déclencheur
*/
function ouvrirModale(el) {
declencheur = el;
overlay.classList.add('est-ouverte');
overlay.setAttribute('aria-hidden', 'false');
// Déplace le focus sur le premier élément interactif
const premierFocusable = overlay.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (premierFocusable) premierFocusable.focus();
}
/**
* Ferme la modale et restitue le focus à l'élément déclencheur.
*/
function fermerModale() {
overlay.classList.remove('est-ouverte');
overlay.setAttribute('aria-hidden', 'true');
// Restitution du focus
if (declencheur) {
declencheur.focus();
declencheur = null;
}
}
// Ouverture au clic sur le bouton principal
btnOuvrir.addEventListener('click', () => ouvrirModale(btnOuvrir));
// Fermeture via le bouton de fermeture
btnFermer.addEventListener('click', fermerModale);
// Fermeture via le bouton Annuler
btnAnnuler.addEventListener('click', fermerModale);
// Fermeture au clic sur l'overlay (en dehors de la boîte)
overlay.addEventListener('click', (e) => {
// On ne ferme que si le clic est sur l'overlay lui-même,
// pas sur un enfant (la boîte de dialogue)
if (e.target === overlay) fermerModale();
});
// Fermeture à la touche Échap
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && overlay.classList.contains('est-ouverte')) {
fermerModale();
}
});
Ce code couvre les trois modes de fermeture attendus par les utilisateurs : le bouton dédié, le clic en dehors de la boîte et la touche Échap. La restitution du focus au déclencheur est indispensable pour les utilisateurs qui naviguent au clavier.
La balise <dialog> HTML5 avec showModal() : la méthode moderne en 2026
Depuis mars 2022, l’élément <dialog> fait partie de la Baseline des navigateurs, ce qui signifie qu’il est supporté par Chrome, Firefox, Safari et Edge dans leurs versions actuelles. En 2026, c’est la méthode recommandée pour créer une fenêtre modale JavaScript.
La méthode showModal() offre gratuitement, sans une seule ligne de JavaScript supplémentaire :
::backdroprole="dialog" et un aria-modal="true" implicitesz-indexSelon Jared Cunha, qui a reconstruit la modale du U.S. Web Design System sur <dialog>, la réécriture est passée d’environ 400 lignes de JavaScript à environ 38 lignes, les comportements d’accessibilité étant désormais gérés nativement par le navigateur.
Code complet avec <dialog> et showModal()
<!-- Bouton déclencheur -->
<button id="ouvrir-dialog" type="button">Ouvrir</button>
<!-- Balise dialog : le navigateur gère le backdrop, le focus et Échap -->
<dialog id="mon-dialog" aria-labelledby="dialog-titre">
<h2 id="dialog-titre">Confirmer la suppression</h2>
<p>Cette action est irréversible. Souhaitez-vous continuer ?</p>
<!--
method="dialog" ferme la boîte à la soumission du formulaire
et stocke la valeur du bouton pressé dans dialog.returnValue
-->
<form method="dialog">
<button type="submit" value="annuler">Annuler</button>
<button type="submit" value="confirmer">Confirmer</button>
</form>
</dialog>
/* Style de la boîte de dialogue */
#mon-dialog {
border: none;
border-radius: 8px;
padding: 1.5rem;
width: min(480px, calc(100vw - 2rem));
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
}
/* Style du fond de scène */
#mon-dialog::backdrop {
background: rgba(0, 0, 0, 0.55);
}
/* Animation d'entrée */
#mon-dialog {
opacity: 0;
transition: opacity 0.2s, overlay 0.2s allow-discrete, display 0.2s allow-discrete;
}
#mon-dialog[open] { opacity: 1; }
@starting-style {
#mon-dialog[open] { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
#mon-dialog { transition: none; }
}
const dialog = document.getElementById('mon-dialog');
const btnOuvrir = document.getElementById('ouvrir-dialog');
// Ouverture : showModal() place le dialog en top layer
btnOuvrir.addEventListener('click', () => dialog.showModal());
// Fermeture au clic sur le backdrop
// (le natif ne ferme pas au clic extérieur par défaut)
dialog.addEventListener('click', (e) => {
if (e.target === dialog) dialog.close();
});
// Lecture du résultat après fermeture
dialog.addEventListener('close', () => {
if (dialog.returnValue === 'confirmer') {
console.log('Action confirmée');
// Votre logique ici
}
});
open pour ouvrir une modale. Il produit un dialogue non modal, sans piège de focus ni arrière-plan inerte. Appelez toujours showModal().Nouveauté 2026 : l’attribut closedby
Mentionné à la Google I/O 2026, l’attribut closedby permet de contrôler comment un <dialog> se ferme, sans JavaScript :
<!-- Se ferme au clic extérieur, à Échap et aux gestes natifs -->
<dialog closedby="any">…</dialog>
<!-- Se ferme uniquement à Échap (comportement par défaut de showModal) -->
<dialog closedby="closerequest">…</dialog>
<!-- Ne se ferme que via le code ou un bouton interne -->
<dialog closedby="none">…</dialog>
Support en juillet 2026 : Chrome/Edge depuis la version 134, Firefox depuis la version 141, Opera depuis la version 119. Safari ne le supporte pas encore. (Source : alsacreations.com)
Bonnes pratiques d’accessibilité pour vos popups JavaScript
Une fenêtre modale inaccessible exclut les utilisateurs de lecteurs d’écran, de navigation au clavier et de technologies d’assistance. Voici les règles à respecter absolument.
Les attributs ARIA essentiels
Pour une modale construite sans <dialog> (avec un <div>) :
| Attribut | Valeur | Rôle |
|---|---|---|
role |
"dialog" |
Identifie l’élément comme une boîte de dialogue |
aria-modal |
"true" |
Indique que le contenu extérieur est inerte |
aria-labelledby |
ID du titre | Donne un nom accessible à la modale |
aria-describedby |
ID de la description | Optionnel : description du contenu |
Pour un <dialog> natif ouvert avec showModal(), role="dialog" et aria-modal="true" sont implicites. Seul aria-labelledby doit être ajouté manuellement.
Le piège de focus (focus trap)
Quand une modale est ouverte, la navigation au clavier (touche Tab) ne doit pas sortir de la boîte. Avec <dialog> et showModal(), ce comportement est géré nativement par le navigateur. Pour une modale custom en <div>, il faut l’implémenter manuellement :
function piegerFocus(modale) {
const selecteursFocusables = [
'a[href]', 'button:not([disabled])', 'input:not([disabled])',
'select:not([disabled])', 'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])'
].join(', ');
const elements = Array.from(modale.querySelectorAll(selecteursFocusables));
const premier = elements[0];
const dernier = elements[elements.length - 1];
modale.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
// Shift+Tab sur le premier élément : aller au dernier
if (e.shiftKey && document.activeElement === premier) {
e.preventDefault();
dernier.focus();
}
// Tab sur le dernier élément : revenir au premier
else if (!e.shiftKey && document.activeElement === dernier) {
e.preventDefault();
premier.focus();
}
});
}
Retour du focus à la fermeture
C’est l’un des points les plus souvent oubliés. Quand la modale se ferme, le focus doit retourner sur l’élément qui l’a ouverte. Avec <dialog> et showModal(), ce comportement est automatique. Avec une modale custom, il faut mémoriser le déclencheur avant l’ouverture et appeler .focus() à la fermeture (comme montré dans le code de la section précédente).
Rendre l’arrière-plan inerte
Pour les modales custom, l’arrière-plan doit être rendu non interactif. La méthode moderne utilise l’attribut inert :
// À l'ouverture
document.querySelectorAll('body > *:not(#overlay)').forEach(el => {
el.inert = true;
});
// À la fermeture
document.querySelectorAll('[inert]').forEach(el => {
el.inert = false;
});
L’attribut inert retire les éléments de l’ordre de tabulation et les masque aux technologies d’assistance, ce qui est plus robuste qu’aria-hidden="true" seul.
Checklist accessibilité rapide
aria-labelledby ?aria-label) ?Les déclencheurs intelligents : délai, scroll et exit intent
Une popup qui s’affiche dès le chargement de la page est à la fois agressive et contre-productive. Les déclencheurs comportementaux permettent d’afficher la modale au bon moment, ce qui améliore à la fois l’expérience utilisateur et les taux de conversion.
Déclencheur après un délai
Le plus simple : afficher la popup après X secondes. Recommandation : attendre au minimum 8 à 15 secondes pour laisser l’utilisateur commencer à lire.
// Affiche la popup après 10 secondes
// et ne la montre qu'une fois par session
function initPopupDelai(delaiMs = 10000) {
if (sessionStorage.getItem('popup-affichee')) return;
setTimeout(() => {
dialog.showModal();
sessionStorage.setItem('popup-affichee', '1');
}, delaiMs);
}
initPopupDelai(10000);
Déclencheur au scroll
Afficher la popup quand l’utilisateur a scrollé jusqu’à un certain pourcentage de la page. C’est un signal d’engagement fort.
function initPopupScroll(seuilPourcent = 50) {
if (sessionStorage.getItem('popup-affichee')) return;
let declenche = false;
function calculerPourcentageScroll() {
const h = document.documentElement;
const b = document.body;
const st = h.scrollTop || b.scrollTop;
const sh = h.scrollHeight || b.scrollHeight;
return (st / (sh - h.clientHeight)) * 100;
}
window.addEventListener('scroll', () => {
if (!declenche && calculerPourcentageScroll() >= seuilPourcent) {
declenche = true;
dialog.showModal();
sessionStorage.setItem('popup-affichee', '1');
window.removeEventListener('scroll', arguments.callee);
}
}, { passive: true });
}
initPopupScroll(50);
Déclencheur exit intent (intention de sortie)
L’exit intent détecte que l’utilisateur s’apprête à quitter la page. Sur desktop, on surveille le mouvement de la souris vers le haut du viewport. Sur mobile, l’événement mouseleave n’existe pas : on utilise un scroll rapide vers le haut comme signal de substitution.
function initExitIntent(callback, options = {}) {
const {
sensibilite = 0, // px depuis le bord supérieur
delaiMinMs = 5000, // délai minimum avant activation
scrollDelta = 60, // px de scroll rapide vers le haut (mobile)
uneSeuleFois = true,
} = options;
let declenche = false;
let dernierScrollY = window.scrollY;
const debut = Date.now();
const declencher = () => {
if (declenche) return;
if (Date.now() - debut < delaiMinMs) return;
if (uneSeuleFois) declenche = true;
callback();
};
// Desktop : mouvement de souris vers le haut
document.addEventListener('mouseout', (e) => {
if (!e.relatedTarget && e.clientY <= sensibilite) declencher();
});
// Mobile : scroll rapide vers le haut
window.addEventListener('scroll', () => {
const y = window.scrollY;
if (dernierScrollY - y > scrollDelta) declencher();
dernierScrollY = y;
}, { passive: true });
}
// Utilisation
initExitIntent(() => {
if (!sessionStorage.getItem('exit-popup-affichee')) {
dialog.showModal();
sessionStorage.setItem('exit-popup-affichee', '1');
}
}, { delaiMinMs: 5000 });
sessionStorage ou localStorage pour ne pas répéter la popup à chaque page ou à chaque visite.Popups et SEO : les règles pour ne pas être pénalisé par Google
Les popups JavaScript ont un impact direct sur votre référencement naturel si elles sont mal configurées. Google applique des sanctions précises, qu’il est indispensable de connaître.
La pénalité interstitiels intrusifs (depuis 2017)
En janvier 2017, Google a déployé une mise à jour ciblant les interstitiels intrusifs sur mobile. Trois configurations déclenchent cette pénalité :
Les sites pénalisés peuvent voir leur trafic mobile chuter de 15 à 40 % selon les observations de terrain.
La mise à jour anti-spam de juin 2026
En juin 2026, Google a renforcé son traitement des popups trompeuses dans le cadre de son update anti-spam. Les popups qui simulent une urgence artificielle, masquent délibérément le bouton de fermeture ou empêchent l’accès au contenu peuvent désormais déclencher une action algorithmique, indépendamment du support (mobile ou desktop).
L’impact sur les Core Web Vitals
Les popups dégradent deux métriques clés :
Ce que Google autorise explicitement
Certaines popups ne sont pas pénalisées :
Les règles à respecter pour rester dans les clous
Consentement RGPD et popups
Si votre popup collecte des données personnelles (email, prénom, etc.), elle doit respecter le RGPD :
Tableau comparatif des méthodes
| Méthode | Personnalisation | Accessibilité | Complexité | Recommandée en 2026 |
|---|---|---|---|---|
alert() / confirm() / prompt() |
Aucune | Basique | Très faible | Non (débogage uniquement) |
| Modale custom HTML/CSS/JS | Totale | À implémenter | Moyenne | Oui (si besoin de contrôle total) |
<dialog> + showModal() |
Totale | Native | Faible | Oui (méthode recommandée) |
<dialog> + attribut open |
Totale | Partielle | Faible | Non (non modal, pas de focus trap) |
window.open() |
Limitée | Mauvaise | Faible | Non (bloqué par les navigateurs) |
Pour aller plus loin côté technique, consultez nos guides pour inspecter une page web et afficher le symbole copyright en HTML. Et si votre objectif derrière ce pop-up est de capter des prospects, Djaboo centralise ensuite vos contacts dans un CRM et transforme vos devis en factures en un clic.
FAQ : 5 questions fréquentes sur les popups JavaScript
1. Quelle est la différence entre `showModal()` et `show()` sur un `<dialog>` ?
showModal() ouvre la boîte en mode modal : le reste de la page devient inerte, le focus est piégé à l’intérieur et la touche Échap ferme automatiquement la boîte. show() ouvre un dialogue non modal : l’utilisateur peut continuer à interagir avec la page, le focus n’est pas piégé. Utilisez showModal() pour les confirmations, formulaires importants et toute interruption nécessitant une réponse. Utilisez show() pour les panneaux d’aide contextuelle ou les notifications non bloquantes.
2. Dois-je ajouter un piège de focus (focus trap) à un `<dialog>` natif ouvert avec `showModal()` ?
Non. Le navigateur gère déjà le confinement du focus via la mise en top layer et l’inertie de l’arrière-plan. Ajouter un piège de focus JavaScript sur un <dialog> natif est non seulement inutile, mais peut supprimer la capacité des utilisateurs clavier à atteindre les contrôles du navigateur (barre d’adresse, onglets). Le focus trap manuel ne s’applique qu’aux modales construites avec des <div> personnalisés.
3. Comment empêcher le scroll de la page derrière une modale ouverte ?
Avec <dialog> et showModal(), le navigateur ne bloque pas le scroll de la page par défaut. La solution CSS la plus propre :
:root:has(dialog[open]) {
overflow: hidden;
scrollbar-gutter: stable; /* évite le saut de mise en page */
}
Sur iOS Safari, overflow: hidden sur le body est peu fiable. La solution plus robuste consiste à capturer window.scrollY à l’ouverture et à appliquer position: fixed avec un top négatif correspondant.
4. L’exit intent fonctionne-t-il sur mobile ?
Pas directement, car les appareils tactiles ne génèrent pas d’événements mouseleave. Les alternatives pour mobile sont le déclencheur au scroll (scroll rapide vers le haut après avoir scrollé au moins 100 px vers le bas) et le déclencheur par inactivité (aucune interaction pendant 20 à 30 secondes). Ces signaux sont moins précis que l’exit intent desktop, mais suffisamment fiables pour être utiles. Sur mobile, un déclencheur au scroll à 60 à 70 % de la page reste la meilleure alternative.
5. Comment éviter que la popup s’affiche à chaque visite ?
Utilisez sessionStorage pour ne pas répéter la popup dans la même session, et localStorage pour une suppression sur une durée plus longue (7 jours, 14 jours, etc.) :
// Ne montre la popup qu'une fois par semaine
function peutAfficherPopup(cle, joursIntervalle = 7) {
const stockage = localStorage.getItem(cle);
if (stockage) {
const joursEcoules = (Date.now() - Number(stockage)) / (1000 * 60 * 60 * 24);
if (joursEcoules < joursIntervalle) return false;
}
localStorage.setItem(cle, String(Date.now()));
return true;
}
if (peutAfficherPopup('ma-popup', 7)) {
dialog.showModal();
}
Maîtriser les popups JavaScript, c’est bien plus que connaître trois méthodes de déclenchement. C’est comprendre les implications en termes d’accessibilité, de performance et de référencement, et choisir la bonne approche selon le contexte. La balise <dialog> avec showModal() est aujourd’hui la solution la plus complète et la moins coûteuse en code : elle gère nativement ce qu’il fallait autrefois implémenter manuellement en des centaines de lignes. Pour les modales plus complexes (animations élaborées, systèmes de design, contraintes d’empilement spécifiques), une implémentation custom reste pertinente, à condition de respecter scrupuleusement les règles d’accessibilité et les directives SEO de Google.
Si vous gérez des projets web et avez besoin d’un outil pour centraliser vos clients, vos équipes et vos workflows, Djaboo est une solution CRM tout-en-un conçue pour les TPE et PME, sans compétences techniques requises.













