TransitionsGratuit

Dialog Curtain

Une fenêtre modale qui s'ouvre en trois temps : le fond s'assombrit, deux rideaux glissent des bords et se rejoignent au centre en 0,6 s, puis le contenu grossit devant eux après 0,5 s — et tout se referme dans l'ordre inverse. Un vrai élément dialog ouvert par showModal (page derrière inerte, focus dans la fenêtre, Échap), 2,3 Ko de JavaScript, état final direct si l'utilisateur réduit les animations.

CSSDialogTransition

Mis à jour le

Rideau de scène

Le contenu apparaît une fois les rideaux fermés.

Survolez ou cliquez la scène pour interagir

Cet effet est gratuit — la catégorie Transitions en contient 52 au total, dont 9 gratuits. Effect.Labs regroupe 840 effets vanilla. Découvrir la catégorie →

Exemples d'usage

Examen blanc · série 12Question 40 / 40

À la prochaine intersection, qui a la priorité ?

A · Moi, je suis sur la route principale B · Les véhicules venant de droite C · Le premier arrivé

Réponse enregistrée. 39 questions répondues avant celle-ci.

① Résultat de l'examen blanc — plateforme de préparation au code de la route

QuandLe candidat valide la 40e question de la série et touche « Voir mon résultat » : les rideaux bleus se ferment sur l'écran de quiz, puis la fenêtre « 37/40 — Admis » apparaît devant eux.
PourquoiLes trois temps créent un petit suspense avant la note, et la fermeture en ordre inverse rend la main proprement sur la série qu'on vient de finir.
RéglagesRideaux en dégradé bleu (#1e3a8a → #1d4ed8) ; carte blanche de 300 px, note de 40 px en vert #15803d, bouton « Revoir mes erreurs » gris clair ; durées et retards du code inchangés.

Cirque Volpi

Sam. 14 mars · 15 h

Vos places

Gradin B · rang 4 · places 12 à 14

84 €

② Places réservées — billetterie en ligne d'un cirque

QuandSur le plan du chapiteau, le spectateur a choisi trois places au gradin B et touche « Réserver 3 places » : les rideaux de piste se ferment, la confirmation s'affiche devant.
PourquoiLe rideau de piste qui se ferme sur la confirmation reprend l'univers du cirque, au moment exact où l'achat est fait.
RéglagesRideaux rouges à plis nets (repeating-linear-gradient de 16, 12 et 8 px) avec une ombre intérieure vers le centre ; carte crème bordée d'or (#fbbf24), titre en serif de 21 px.

COMPAGNIE DU GRENIER

Troupe de théâtre amateur · saison 2027

Spectacle de printempsLe Malade imaginaireMolière · 3 actes

● La mise en scène a publié la distribution

Première répétition mardi à 20 h · salle polyvalente

③ Rôle attribué — appli d'une troupe de théâtre amateur

QuandLe metteur en scène publie la distribution ; la comédienne touche « Découvrir mon rôle » : les rideaux rouges se ferment, la fenêtre annonce « Toinette — Le Malade imaginaire · 14 répétitions ».
PourquoiLe rideau de scène est l'univers même de la troupe ; la fenêtre n'apparaît qu'une fois les rideaux fermés, on ne devine pas le rôle à moitié pendant l'animation.
RéglagesRideaux de velours bordeaux à plis (repeating-linear-gradient de #5b0f1f à #881337) bordés d'une frange dorée de 10 px ; carte presque noire de 320 px bordée d'or, rôle en serif de 34 px ; fenêtre ouverte par le bouton « Découvrir mon rôle ».

Comment ça marche

La fenêtre est un élément <dialog> plein écran (position: fixed, inset: 0, 100 % × 100 %, sans bordure ni fond) qui contient quatre calques : un fond .dialog-curtain-backdrop, deux rideaux .dialog-curtain-left et .dialog-curtain-right de 50 % de large chacun, et la carte .dialog-curtain-content centrée par translate(-50%, -50%). Au repos, les rideaux attendent hors champ (translateX(-100%) et translateX(100%)), le fond est transparent et la carte vaut opacity: 0 et scale(0.8). La classe .dialog-curtain-open donne l'état d'arrivée : fond noir à 50 %, rideaux à translateX(0), carte à 1 et 1.

Les trois temps viennent des durées et des retards : fond en 0,6 s, rideaux en 0,6 s sur cubic-bezier(0.65, 0, 0.35, 1), carte en 0,4 s avec transition-delay: 0.5s et un léger rebond. Mesuré 250 ms après l'ouverture : rideau gauche encore à −334 px sur 450, fond à 0,35, carte invisible ; à 700 ms, rideaux fermés et carte à 0,8 ; tout est en place vers 0,9 s (mêmes valeurs dans Chromium 151, Firefox 153 et WebKit 26.5). La fermeture pose .dialog-curtain-closing, qui inverse l'ordre : la carte s'efface en 0,2 s, puis rideaux et fond repartent avec 0,15 s de retard. Le script laisse 700 ms à cette sortie avant d'appeler dialog.close().

Le script (2,3 Ko) relie chaque bouton .dialog-curtain-btn à sa fenêtre par aria-controls, ce qui permet plusieurs fenêtres dans la même page. À l'ouverture, il appelle showModal(), lit offsetWidth puis seulement ajoute la classe : la fenêtre sort de display: none, et sans ce calcul intermédiaire le navigateur n'aurait pas d'état de départ et afficherait directement l'état final, sans aucun des trois temps. Il intercepte l'évènement cancel (Échap) pour jouer la fermeture animée au lieu d'une disparition sèche, ferme aussi au clic sur le fond ou sur les rideaux — une fois fermés, ils couvrent tout l'écran — et remet les classes à zéro à chaque évènement close, quelle qu'en soit l'origine. La vignette .dialog-curtain-preview rejoue les rideaux en petit dans la page ; elle est facultative.

Accessibilité

  • Fenêtre modale réelle : showModal() rend le reste de la page inerte (ni clic ni tabulation derrière), place la fenêtre au-dessus de tout et donne le focus à son premier bouton, « Fermer » (mesuré dans les trois moteurs). À la fermeture, le focus revient au bouton qui l'a ouverte (mesuré dans Chromium et Firefox ; dans Safari, quand l'ouverture s'est faite au clavier). La fenêtre est nommée par son titre (aria-labelledby) et le bouton annonce aria-haspopup="dialog".
  • Clavier : Échap ferme avec l'animation inverse ; un second Échap pendant la sortie ferme aussitôt (mesuré : fenêtre fermée, réouverture normale ensuite). La tabulation reste dans la fenêtre : dans Chromium, elle passe par la barre du navigateur puis revient sur « Fermer », jamais sur la page derrière.
  • Mouvement réduit : sous prefers-reduced-motion, toutes les transitions de la scène et de la fenêtre sont coupées : les rideaux et la carte prennent directement leur état, et la fenêtre se ferme sans délai (mesuré : carte à 1 dès 250 ms, fermée 200 ms après Échap).
  • Contraste : texte blanc sur la carte (#1e1e2e → #2d2b55) à 16,4 et 13,2:1 ; le paragraphe à 70 % d'opacité reste au-dessus de 7:1. Le bouton « Fermer », blanc sur #6366f1, est à 4,47:1 pour un texte de 13,6 px en gras : juste sous le seuil AA, foncez-le en #4f46e5 (6,29:1) si besoin.

Compatibilité navigateur

Élément <dialog> et showModal(), pseudo-élément ::backdrop, transitions CSS + 2,3 Ko de JavaScript, aucune dépendance. Mesuré dans Chromium 151, Firefox 153 et WebKit 26.5 : mêmes trois temps à l'ouverture, même fermeture inverse, Échap animé, focus dans la fenêtre puis rendu au bouton, deux fenêtres indépendantes dans la même page. showModal() existe depuis Chrome 37, Firefox 98 et Safari 15.4 ; la propriété inset depuis Chrome 87 et Safari 14.1.

Chrome 87+✓ Complet
Firefox 98+✓ Complet
Safari 15.4+✓ Complet
Edge 87+✓ Complet
Mobile iOS 15.4+✓ Complet
Android Chrome 87+✓ Complet

Sans l'élément dialog (navigateurs antérieurs à 2022), le script ne relie pas le bouton et la fenêtre reste fermée : prévoyez un lien vers une page de confirmation si le contenu est indispensable. Sans JavaScript, même chose : le bouton ne fait rien. Sous mouvement réduit, la fenêtre s'ouvre et se ferme sans animation, avec les mêmes garanties de focus.

Le code

Copiez les trois blocs dans votre page. Aucune dépendance.

HTML
<div class="dialog-curtain-stage">
  <div class="dialog-curtain-preview" aria-hidden="true">
    <div class="dialog-curtain-preview-left"></div>
    <div class="dialog-curtain-preview-right"></div>
    <span class="dialog-curtain-preview-label">Rideau de scène</span>
  </div>
  <button class="dialog-curtain-btn" type="button" aria-haspopup="dialog" aria-controls="dialogCurtainModal">Ouvrir la fenêtre</button>
  <dialog class="dialog-curtain-modal" id="dialogCurtainModal" aria-labelledby="dialogCurtainTitle">
    <div class="dialog-curtain-backdrop"></div>
    <div class="dialog-curtain-left"></div>
    <div class="dialog-curtain-right"></div>
    <div class="dialog-curtain-content">
      <h3 id="dialogCurtainTitle">Rideau de scène</h3>
      <p>Le contenu apparaît une fois les rideaux fermés.</p>
      <button class="dialog-curtain-close" type="button">Fermer</button>
    </div>
  </dialog>
</div>
CSS
.dialog-curtain-stage,
.dialog-curtain-stage *,
.dialog-curtain-stage *::before,
.dialog-curtain-stage *::after {
  box-sizing: border-box
}

.dialog-curtain-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.dialog-curtain-btn {
  padding: 10px 24px;
  font-size: 0.85rem;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  background: #6366f1;
  color: white;
  transition: background 0.2s, transform 0.1s;
}

.dialog-curtain-btn:hover {
  background: #4f46e5;
}

.dialog-curtain-btn:active {
  transform: scale(0.96);
}

.dialog-curtain-preview {
  position: relative;
  width: 200px;
  height: 120px;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: #0a0a1a;
}

.dialog-curtain-preview-left,
.dialog-curtain-preview-right {
  position: absolute;
  top: 0;
  width: 50%;
  height: 100%;
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

.dialog-curtain-preview-left {
  left: 0;
  background: linear-gradient(135deg, #312e81, #4338ca);
  transform: translateX(-100%);
}

.dialog-curtain-preview-right {
  right: 0;
  background: linear-gradient(225deg, #312e81, #4338ca);
  transform: translateX(100%);
}

.dialog-curtain-preview.dialog-curtain-active .dialog-curtain-preview-left {
  transform: translateX(0);
}

.dialog-curtain-preview.dialog-curtain-active .dialog-curtain-preview-right {
  transform: translateX(0);
}

.dialog-curtain-preview-label {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.8);
  opacity: 0;
  color: white;
  font-weight: 600;
  font-size: 0.75rem;
  z-index: 2;
  transition: opacity 0.3s ease 0.4s, transform 0.3s ease 0.4s;
  white-space: nowrap;
}

.dialog-curtain-preview.dialog-curtain-active .dialog-curtain-preview-label {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

.dialog-curtain-left,
.dialog-curtain-right {
  width: 50%;
  height: 100%;
  background: linear-gradient(135deg, #1a1a2e, #0f172a);
}

.dialog-curtain-left {
  transform-origin: left center;
}

.dialog-curtain-right {
  transform-origin: right center;
}

.dialog-curtain-modal {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  margin: 0;
  padding: 0;
  border: none;
  background: transparent;
  color: #fff;
  overflow: hidden;
  font-family: sans-serif;
}

/* Le fond assombri animé est .dialog-curtain-backdrop : celui du navigateur reste transparent. */
.dialog-curtain-modal::backdrop {
  background: transparent;
}

.dialog-curtain-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0);
  transition: background 0.6s ease;
}

.dialog-curtain-modal.dialog-curtain-open .dialog-curtain-backdrop {
  background: rgba(0, 0, 0, 0.5);
}

.dialog-curtain-modal .dialog-curtain-left,
.dialog-curtain-modal .dialog-curtain-right {
  position: absolute;
  top: 0;
  width: 50%;
  height: 100%;
  transition: transform 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}

.dialog-curtain-modal .dialog-curtain-left {
  left: 0;
  background: linear-gradient(135deg, #1e1b4b, #312e81);
  transform: translateX(-100%);
}

.dialog-curtain-modal .dialog-curtain-right {
  right: 0;
  background: linear-gradient(225deg, #1e1b4b, #312e81);
  transform: translateX(100%);
}

.dialog-curtain-modal.dialog-curtain-open .dialog-curtain-left {
  transform: translateX(0);
}

.dialog-curtain-modal.dialog-curtain-open .dialog-curtain-right {
  transform: translateX(0);
}

.dialog-curtain-content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.8);
  opacity: 0;
  background: linear-gradient(135deg, #1e1e2e, #2d2b55);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 20px;
  padding: 32px;
  color: white;
  text-align: center;
  z-index: 2;
  transition: opacity 0.4s ease 0.5s, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 0.5s;
  min-width: 200px;
}

.dialog-curtain-modal.dialog-curtain-open .dialog-curtain-content {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

.dialog-curtain-modal.dialog-curtain-closing .dialog-curtain-content {
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.8);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.dialog-curtain-modal.dialog-curtain-closing .dialog-curtain-left {
  transform: translateX(-100%);
  transition-delay: 0.15s;
}

.dialog-curtain-modal.dialog-curtain-closing .dialog-curtain-right {
  transform: translateX(100%);
  transition-delay: 0.15s;
}

.dialog-curtain-modal.dialog-curtain-closing .dialog-curtain-backdrop {
  background: rgba(0, 0, 0, 0);
  transition-delay: 0.15s;
}

.dialog-curtain-content h3 {
  font-size: 1.2rem;
  margin: 0 0 8px;
}

.dialog-curtain-content p {
  margin: 0 0 16px;
  opacity: 0.7;
  font-size: 0.85rem;
}

.dialog-curtain-close {
  padding: 8px 20px;
  background: #6366f1;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  font-weight: 600;
  font-size: 0.85rem;
}

.dialog-curtain-close:focus-visible {
  outline: 2px solid #c7d2fe;
  outline-offset: 3px;
}

/* Mouvement réduit : pas de rideaux qui glissent, chaque élément prend directement son état. */
@media (prefers-reduced-motion: reduce) {
  .dialog-curtain-stage *,
  .dialog-curtain-modal *,
  .dialog-curtain-modal *::before,
  .dialog-curtain-modal *::after {
    transition: none !important;
  }
}
JavaScript (fx-0716)
(function() {
  /* Dialog Curtain : un vrai <dialog> ouvert en modale (showModal) — le reste de la page devient
     inerte, le focus entre dans la fenêtre et revient au bouton à la fermeture. Les classes
     ne pilotent que l'animation en trois temps : fond, rideaux, puis contenu. */
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');

  document.querySelectorAll('.dialog-curtain-btn[aria-controls]').forEach(function(btn) {
    var dialog = document.getElementById(btn.getAttribute('aria-controls'));
    if (!dialog || typeof dialog.showModal !== 'function') return;
    var stage = btn.closest('.dialog-curtain-stage');
    var preview = stage && stage.querySelector('.dialog-curtain-preview');
    var timer = null;

    function reset() {
      clearTimeout(timer);
      timer = null;
      dialog.classList.remove('dialog-curtain-open', 'dialog-curtain-closing');
    }

    function open() {
      if (dialog.open) return;
      reset();
      dialog.showModal();
      void dialog.offsetWidth; /* point de départ des transitions : rideaux ouverts, contenu caché */
      dialog.classList.add('dialog-curtain-open');
      if (preview) preview.classList.add('dialog-curtain-active');
    }

    function close() {
      if (!dialog.open || timer) return;
      dialog.classList.add('dialog-curtain-closing');
      if (preview) preview.classList.remove('dialog-curtain-active');
      timer = setTimeout(function() {
        dialog.close();
      }, reduce.matches ? 0 : 700);
    }

    btn.addEventListener('click', open);
    /* Une fois fermés, les rideaux recouvrent tout le fond : un clic dessus ferme aussi. */
    dialog.querySelectorAll('.dialog-curtain-close, .dialog-curtain-backdrop, .dialog-curtain-left, .dialog-curtain-right').forEach(function(el) {
      el.addEventListener('click', close);
    });
    /* Échap : la fermeture passe par l'animation inverse au lieu d'être immédiate. */
    dialog.addEventListener('cancel', function(e) {
      e.preventDefault();
      close();
    });
    /* Quelle que soit la façon dont la fenêtre se ferme, elle repart propre. */
    dialog.addEventListener('close', function() {
      reset();
      if (preview) preview.classList.remove('dialog-curtain-active');
    });
  });
})();

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
Rideaux (CSS — .dialog-curtain-modal .dialog-curtain-left et -right) dégradés #1e1b4b → #312e81, 0,6 s sur cubic-bezier(0.65, 0, 0.35, 1) Un repeating-linear-gradient donne des plis de velours ; 0,9 s rend la fermeture plus théâtrale. Changez les deux rideaux ensemble.
Retard du contenu (CSS — transition-delay: 0.5s sur .dialog-curtain-content) 0,5 s : la carte arrive quand les rideaux finissent 0,2 s fait arriver la carte pendant que les rideaux glissent ; au-delà de 0,6 s, un temps mort s'installe entre rideaux fermés et contenu.
Durée de sortie (JS — 700 dans close()) 700 ms Elle doit couvrir à peu près la sortie CSS (0,15 s de retard + 0,6 s des rideaux) : à 700 ms, les rideaux sont à 99 % de leur course, rien de visible ne manque. Si vous allongez les rideaux, allongez ce délai, sinon la fenêtre disparaît avant leur retour.
Fond assombri (CSS — rgba(0, 0, 0, 0.5) sur .dialog-curtain-open .dialog-curtain-backdrop) noir à 50 % Il ne se voit que pendant le glissement des rideaux, qui couvrent ensuite tout l'écran. Pour garder la page visible autour, réduisez la largeur des rideaux (40 % chacun) : le fond prend alors tout son sens.
Fermeture au clic (JS — liste .dialog-curtain-close, .dialog-curtain-backdrop, .dialog-curtain-left, .dialog-curtain-right) bouton, fond et rideaux ferment Retirez les rideaux de la liste pour une confirmation qu'on ne doit pas fermer par erreur ; gardez au moins le bouton et Échap.
Vignette d'aperçu (HTML — .dialog-curtain-preview) 200 × 120 px, rideaux en petit et libellé Supprimez-la sans toucher au script : elle ne sert qu'à montrer le motif dans la page. Le bouton suffit à ouvrir la fenêtre.

FAQ

Les rideaux se ferment au lieu de s'ouvrir : est-ce voulu ?

Oui. Les rideaux partent des bords et se rejoignent au centre, comme un rideau de scène qui tombe ; la fenêtre se présente ensuite devant eux, et ils se rouvrent à la fermeture. Pour l'effet inverse, des rideaux qui s'écartent pour découvrir le contenu, placez la carte sous les rideaux (z-index), mettez les rideaux à translateX(0) au repos et à ±100% dans .dialog-curtain-open, et retirez le retard de la carte.

Pourquoi un élément dialog natif plutôt qu'une div plein écran ?

Une div en position: fixed se voit comme une fenêtre, mais la page derrière reste cliquable et atteignable au clavier, Échap ne fait rien et le focus reste sur le bouton caché dessous. showModal() règle tout cela d'un coup : page inerte, focus dans la fenêtre puis rendu au bouton, Échap, affichage au-dessus de tout z-index. Le script n'ajoute que l'animation.

Pourquoi le script lit-il offsetWidth juste après showModal() ?

Juste avant showModal(), la fenêtre est en display: none : elle n'a aucun état affiché d'où partir. Si la classe .dialog-curtain-open arrive dans la même image, le navigateur calcule directement l'état final et les transitions ne se jouent pas (rideaux déjà fermés, carte déjà visible). Lire offsetWidth force un calcul de l'état « rideaux ouverts, carte cachée » ; la classe ajoutée ensuite déclenche les trois temps. @starting-style ferait le même travail dans les navigateurs récents.