Quelle différence entre une transition CSS, une animation et une view transition ?

Une transition CSS anime une propriété entre deux états quand elle change, au survol ou quand une classe est ajoutée, alors qu'une animation CSS joue ses images clés d'elle-même. Les view transitions vont plus loin : la View Transitions API capture l'ancien et le nouvel état de la page et anime le passage de l'un à l'autre, si bien qu'une carte peut s'agrandir jusqu'à devenir sa page de détail.

Comment choisir, performances et accessibilité
  • Utilisez Crossfade Transition ou Fade Through entre deux états d'un contenu, Circle Reveal ou Curtain Open pour des changements de section spectaculaires, Dark Mode pour un sélecteur de thème, et Popover Bloom ou Dialog Curtain pour les menus et fenêtres.
  • Gardez la plupart des transitions d'interface entre 150 et 400 ms environ, animez transform et opacity pour éviter les décalages de mise en page, et laissez les navigateurs sans View Transitions API basculer instantanément.
  • Fade Through, Crossfade Transition, Dark Mode et plusieurs autres transitions sont gratuits avec leur code ; les autres sont inclus dans Premium.
Page A
Page B

Fade Through

La page s'efface progressivement vers le noir puis révèle la nouvelle page.

CSS Opacity Smooth
Page de l’effet →
Voir le code source
HTML
<div class="ft-stage">
  <div class="transition-preview">
    <div class="transition-content old-content">Page A</div>
    <div class="transition-content new-content">Page B</div>
    <div class="transition-overlay fade-through-overlay"></div>
  </div>
  <button type="button" class="trigger-btn">Lancer la transition</button>
</div>
CSS
.ft-stage,
.ft-stage *,
.ft-stage *::before,
.ft-stage *::after {
  box-sizing: border-box
}

/* Racine de l'effet : écran en haut, bouton dessous. */
.ft-stage {
  --space-4: 1rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 300px;
  padding: 16px;
  position: relative;
  background: rgb(26, 26, 36);
  color: #fff;
  font-family: sans-serif
}

.ft-stage .transition-preview {
  position: relative;
  width: 100%;
  height: 200px;
  border-radius: 12px;
  overflow: hidden;
  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)
}

.ft-stage .transition-content {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.2rem;
  font-weight: 600;
  color: white
}

.ft-stage .transition-content.old-content {
  background: linear-gradient(135deg, #4f46e5, #7c3aed)
}

.ft-stage .transition-content.new-content {
  background: linear-gradient(135deg, #0e7490, #047857);
  opacity: 0;
  visibility: hidden
}

/* Page B affichée : la classe .is-b est posée sur l'écran au milieu de la transition, sous le calque,
   et retirée au clic suivant (retour à A). */
.ft-stage .transition-preview.is-b .old-content {
  opacity: 0;
  visibility: hidden
}

.ft-stage .transition-preview.is-b .new-content {
  opacity: 1;
  visibility: visible
}

.ft-stage .trigger-btn {
  padding: 12px 24px;
  font-size: 0.9rem;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-family: inherit;
  background: linear-gradient(135deg, #4f46e5, #7c3aed);
  color: white;
  transition: all 0.3s ease;
  margin-top: var(--space-4)
}

.ft-stage .trigger-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 30px rgba(99, 102, 241, 0.4)
}

.ft-stage .trigger-btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none
}

.ft-stage .transition-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 10
}

.ft-stage .fade-through-overlay {
  background: #0a0a0f;
  opacity: 0
}

.ft-stage .fade-through-overlay.animating {
  animation: fadeThrough 1.2s ease-in-out
}

@keyframes fadeThrough {
  0% {
    opacity: 0;
  }

  40% {
    opacity: 1;
  }

  60% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

/* Mouvement réduit : pas de calque, les deux pages s'échangent en fondu de 150 ms. */
@media (prefers-reduced-motion: reduce) {
  .ft-stage .transition-content {
    transition: opacity .15s, visibility .15s
  }
}
JavaScript
(function () {
  /* fx-0685 — Fade Through : un voile noir monte jusqu'à couvrir la page A, la page change dessous,
     puis le calque se retire sur la nouvelle page. Un clic mène à la page B et y reste ; le clic suivant ramène à A. */
  var SWAP_AT = 600;   // échange A ⇄ B, au milieu : le calque couvre tout l'écran
  var END_AT = 1200;   // fin de l'animation du calque (même durée que le CSS)
  var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

  document.querySelectorAll('.ft-stage').forEach(function (stage) {
    var screen = stage.querySelector('.transition-preview');
    var layer = stage.querySelector('.fade-through-overlay');
    var btn = stage.querySelector('.trigger-btn');
    if (!screen || !layer || !btn) return;
    var busy = false;

    btn.addEventListener('click', function () {
      if (busy) return;
      // Mouvement réduit : pas de calque, échange direct (fondu de 150 ms dans le CSS)
      if (reduceMotion.matches) {
        screen.classList.toggle('is-b');
        return;
      }
      busy = true;
      btn.setAttribute('aria-disabled', 'true'); // et non disabled, qui ferait perdre le focus clavier
      layer.classList.add('animating');
      setTimeout(function () {
        screen.classList.toggle('is-b');
      }, SWAP_AT);
      setTimeout(function () {
        layer.classList.remove('animating');
        btn.removeAttribute('aria-disabled');
        busy = false;
      }, END_AT);
    });
  });
})();

Slide Over

Le nouveau contenu glisse par-dessus l'ancien de gauche à droite.

CSS Transform Slide

Circle Reveal

Un cercle s'agrandit depuis le centre pour révéler la nouvelle page.

CSS Clip-path Circle

Curtain Open

Deux panneaux s'ouvrent comme des rideaux de théâtre.

CSS Transform Theatrical

Diagonal Wipe

Une ligne diagonale balaye l'écran pour révéler le nouveau contenu.

CSS Clip-path Wipe

Pixelate Dissolve

L'écran se décompose en pixels qui apparaissent et disparaissent.

CSS Grid Retro

Flip Page

La page se retourne comme dans un livre.

CSS JS 3D Perspective

Zoom Through

Zoom vers le centre de l'écran puis zoom arrière sur la nouvelle page.

CSS Scale Zoom

Blinds Effect

Des stores horizontaux se ferment puis s'ouvrent pour révéler la nouvelle page.

CSS Transform Blinds

Bubble Morph

Une forme se transforme et s'agrandit pour couvrir l'écran puis rétrécit.

CSS JS Morph Shape
État A
Page d'accueil
Cliquez pour basculer

Crossfade Transition

Fondu enchaine fluide entre deux états. Simule le crossfade natif de la View Transitions API.

CSS Crossfade Premium
Page de l’effet →
Voir le code source
HTML
<div class="crossfade-stage">
  <div class="crossfade-frame" role="button" tabindex="0" aria-pressed="false">
    <div class="crossfade-state crossfade-state-a">
      <div class="crossfade-icon" aria-hidden="true">&#9733;</div>
      <div>
        <div style="font-size: 1rem; font-weight: 800;">État A</div>
        <div style="font-size: 0.65rem; opacity: 0.7; margin-top: 2px;">Page d'accueil</div>
      </div>
    </div>
    <div class="crossfade-state crossfade-state-b" aria-hidden="true">
      <div class="crossfade-icon" aria-hidden="true">&#9829;</div>
      <div>
        <div style="font-size: 1rem; font-weight: 800;">État B</div>
        <div style="font-size: 0.65rem; opacity: 0.7; margin-top: 2px;">Page profil</div>
      </div>
    </div>
  </div>
  <span class="crossfade-hint">Cliquez pour basculer</span>
</div>
CSS
.crossfade-stage,
.crossfade-stage *,
.crossfade-stage *::before,
.crossfade-stage *::after {
  box-sizing: border-box
}

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

.crossfade-hint {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  font-size: .65rem;
  color: rgba(255, 255, 255, .35);
  text-transform: uppercase;
  letter-spacing: 2px;
  white-space: nowrap;
  pointer-events: none
}

.crossfade-frame {
  width: 280px;
  height: 180px;
  border-radius: 16px;
  position: relative;
  overflow: hidden;
  cursor: pointer
}

.crossfade-frame:focus-visible {
  outline: 2px solid #f9a8d4;
  outline-offset: 4px
}

.crossfade-state {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  font-size: 1.1rem;
  letter-spacing: 1px;
  transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1);
  border-radius: 16px
}

.crossfade-state-a {
  background: linear-gradient(135deg, #6366f1, #8b5cf6, #a855f7);
  opacity: 1
}

.crossfade-state-b {
  background: linear-gradient(135deg, #f43f5e, #ec4899, #d946ef);
  opacity: 0
}

.crossfade-frame.active .crossfade-state-a {
  opacity: 0
}

.crossfade-frame.active .crossfade-state-b {
  opacity: 1
}

.crossfade-icon {
  width: 50px;
  height: 50px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.2);
  margin-right: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  backdrop-filter: blur(10px)
}

/* Mouvement réduit : le fondu reste (aucun déplacement), raccourci à 0,15 s */
@media (prefers-reduced-motion: reduce) {
  .crossfade-state {
    transition-duration: .15s
  }
}
JavaScript
(function() {
  /* fx-0695 : Crossfade Transition — chaque .crossfade-frame de la page bascule entre l'état A et l'état B
     (clic, Entrée ou Espace) et reste sur l'état atteint ; l'action suivante fait le chemin inverse. */
  document.querySelectorAll('.crossfade-frame').forEach(function(frame) {
    var a = frame.querySelector('.crossfade-state-a');
    var b = frame.querySelector('.crossfade-state-b');
    function toggle() {
      var on = frame.classList.toggle('active');
      frame.setAttribute('aria-pressed', on ? 'true' : 'false');
      if (a) a.setAttribute('aria-hidden', on ? 'true' : 'false');
      if (b) b.setAttribute('aria-hidden', on ? 'false' : 'true');
    }
    frame.addEventListener('click', toggle);
    frame.addEventListener('keydown', function(e) {
      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
    });
  });
})();

Slide Transition

Le contenu glisse vers la gauche tandis que le nouveau contenu arrive par la droite, comme une navigation entre pages.

CSS Slide Navigation

Scale Morph

L'élément change de taille, de forme et de couleur avec une courbe d'animation élastique.

CSS Morph Premium

Shared Element

Un élément partagé se déplace d'une position à l'autre, imitant la transition d'élément partagé de l'API.

CSS Shared Premium

Fade & Slide Up

Le contenu sort en fondu vers le haut et le nouveau contenu monte depuis le bas. Idéal pour les profils et cartes.

CSS Fade Slide

Flip Transition

La carte effectue une rotation 3D pour révéler un contenu different au verso. Effet classique et élégant.

CSS 3D Flip

Zoom Transition

Effet de zoom avant qui agrandit la vue pour passer à un nouvel état détaillé. Parfait pour les galeries.

CSS Zoom Premium

Circular Reveal

Au clic, un cercle s'agrandit depuis le centre de la carte (clip-path) et révèle le second état ; un 2e clic le referme. Idéal pour les bascules de thème.

CSS JS Clip-path Premium

Stagger List

Au clic, les éléments de la liste sortent un par un avec un délai croissant, puis reviennent en cascade au 2e clic. Accepte vos propres éléments.

CSS JS Stagger List

Page Turn

Simule un effet de page qui tourne en 3D pour révéler la page suivante. Inspiré des liseuses et livres numériques.

CSS 3D Premium

Dark Mode

Thème sombre actif

Dark Mode

Toggle de thème avec révélation circulaire via clip-path: circle() depuis le bouton en haut à droite.

CSS clip-path Toggle
Page de l’effet →
Voir le code source
HTML
<div class="th-circular-root">
  <div class="th-circular-scene">
    <button class="th-circular-toggle" type="button" aria-pressed="false" aria-label="Thème clair">
      <svg class="th-circular-sun" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
      <svg class="th-circular-moon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
    </button>
    <div class="th-circular-dark">
      <div class="th-circular-card">
        <h3>Dark Mode</h3>
        <p>Thème sombre actif</p>
        <div class="th-circular-card-bar"><div class="th-circular-card-bar-fill"></div></div>
      </div>
    </div>
    <div class="th-circular-light" aria-hidden="true">
      <div class="th-circular-card">
        <h3>Light Mode</h3>
        <p>Thème clair actif</p>
        <div class="th-circular-card-bar"><div class="th-circular-card-bar-fill"></div></div>
      </div>
    </div>
  </div>
</div>
CSS
.th-circular-root,
.th-circular-root *,
.th-circular-root *::before,
.th-circular-root *::after {
  box-sizing: border-box
}

.th-circular-root {
  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;
}

.th-circular-scene {
  width: 300px;
  height: 220px;
  position: relative;
  border-radius: 16px;
  overflow: hidden;
}

/* Les deux thèmes sont deux calques superposés de la taille de la scène */
.th-circular-dark,
.th-circular-light {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.th-circular-dark {
  background: #0f172a;
  z-index: 1;
}

/* Le clair attend, réduit à un cercle de rayon 0 centré sur le bouton (30 px du bord haut et du bord droit) */
.th-circular-light {
  background: #f8fafc;
  z-index: 2;
  clip-path: circle(0% at calc(100% - 30px) 30px);
  transition: clip-path 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}

.th-circular-scene.active .th-circular-light {
  clip-path: circle(150% at calc(100% - 30px) 30px);
}

.th-circular-dark h3 {
  color: #fff;
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
}

.th-circular-dark p {
  color: rgba(255, 255, 255, 0.5);
  font-size: 0.75rem;
  margin: 0;
}

.th-circular-light h3 {
  color: #0f172a;
  font-size: 1rem;
  font-weight: 700;
  margin: 0;
}

.th-circular-light p {
  color: #64748b;
  font-size: 0.75rem;
  margin: 0;
}

.th-circular-toggle {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: none;
  padding: 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
  background: rgba(255, 255, 255, 0.1);
  transition: background-color 0.3s ease;
}

.th-circular-scene.active .th-circular-toggle {
  background: rgba(0, 0, 0, 0.08);
}

.th-circular-toggle:focus-visible {
  outline: 2px solid #a78bfa;
  outline-offset: 2px;
}

.th-circular-toggle svg {
  width: 18px;
  height: 18px;
  stroke-width: 2;
  fill: none;
}

.th-circular-sun {
  stroke: #fbbf24;
}

.th-circular-moon {
  stroke: #6366f1;
  display: none;
}

.th-circular-scene.active .th-circular-sun {
  display: none;
}

.th-circular-scene.active .th-circular-moon {
  display: block;
}

.th-circular-card {
  width: 220px;
  padding: 16px;
  border-radius: 12px;
}

.th-circular-dark .th-circular-card {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.08);
}

.th-circular-light .th-circular-card {
  background: #fff;
  border: 1px solid #e2e8f0;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}

.th-circular-card-bar {
  height: 6px;
  border-radius: 3px;
  margin-top: 10px;
}

.th-circular-dark .th-circular-card-bar {
  background: rgba(255, 255, 255, 0.08);
}

.th-circular-light .th-circular-card-bar {
  background: #e2e8f0;
}

.th-circular-card-bar-fill {
  height: 100%;
  width: 65%;
  border-radius: 3px;
  background: linear-gradient(90deg, #8b5cf6, #a78bfa);
}

/* Mouvement réduit : pas de cercle qui grandit, un fondu de 150 ms */
@media (prefers-reduced-motion: reduce) {
  .th-circular-light,
  .th-circular-scene.active .th-circular-light {
    clip-path: none;
  }
  .th-circular-light {
    opacity: 0;
    transition: opacity 0.15s linear;
  }
  .th-circular-scene.active .th-circular-light {
    opacity: 1;
  }
}
JavaScript
(function () {
  /* fx-0705 Dark Mode : le bouton de chaque .th-circular-scene pose ou retire .active (le CSS fait la transition) */
  document.querySelectorAll('.th-circular-scene').forEach(function (scene) {
    var btn = scene.querySelector('.th-circular-toggle');
    var dark = scene.querySelector('.th-circular-dark');
    var light = scene.querySelector('.th-circular-light');
    if (!btn) return;
    function sync() {
      var on = scene.classList.contains('active');
      btn.setAttribute('aria-pressed', on ? 'true' : 'false');
      if (dark) dark.setAttribute('aria-hidden', on ? 'true' : 'false');
      if (light) light.setAttribute('aria-hidden', on ? 'false' : 'true');
    }
    btn.addEventListener('click', function () {
      scene.classList.toggle('active');
      sync();
    });
    sync();
  });
})();

Smooth Fade

Transition entre thèmes clair et sombre par fondu croisé d'opacité ; l'icône lune pivote et laisse place au soleil.

CSS JS Fade Opacity

Wipe Transition

Balayage horizontal avec clip-path: inset() et ligne de séparation lumineuse qui traverse l'écran.

CSS Wipe clip-path

CSS Variables

Transition de thème pilotée par six CSS custom properties : les couleurs qui les lisent changent en douceur.

CSS Variables Morph

Icon Toggle

Bouton de thème avec morphing rotatif entre les icônes soleil et lune. Rotation et scale combinés.

CSS Icon Morph

Card Theme

Carte complète avec avatar, texte et boutons dont chaque élément transitionne entre les deux thèmes.

CSS Card Transition

Gradient Shift

Fond gradient qui passe de teintes froides nocturnes à des tons chauds dorés en 0.8s.

CSS Gradient Ambient

Adaptive UI

Interface miniature complète (nav, contenu, blocs) qui bascule entre thème sombre et clair via le bouton soleil/lune de sa barre.

CSS JS UI Adaptive

Split Compare

Les deux thèmes superposés et un séparateur « VS » qu'on fait glisser (souris, doigt, clavier) pour comparer.

CSS JS Compare

Pixel Transition

Transition pixelisée : des blocs colorés apparaissent en cascade aléatoire pour révéler le nouveau thème.

CSS JS Pixel
La bulle éclot depuis son ancre

Popover Bloom

L'élément s'ouvre en fleur depuis son point d'ancrage avec scale + opacity via @starting-style.

CSS @starting-style Popover
Page de l’effet →
Voir le code source
HTML
<div class="popover-bloom-stage">
  <button class="popover-bloom-btn" type="button" popovertarget="popoverBloomBox">Afficher la bulle</button>
  <div class="popover-bloom-box" id="popoverBloomBox" popover>La bulle éclot depuis son ancre</div>
</div>
CSS
.popover-bloom-stage,
.popover-bloom-stage *,
.popover-bloom-stage *::before,
.popover-bloom-stage *::after {
  box-sizing: border-box
}

.popover-bloom-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;
}

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

.popover-bloom-btn:hover {
  background: #4338ca;
}

.popover-bloom-btn:active {
  transform: scale(0.96);
}

/* La bulle est un popover : elle vit dans la couche supérieure (jamais coupée par un overflow: hidden,
   jamais sous un z-index voisin) et se place au-dessus du bouton qui l'ouvre grâce à l'ancrage CSS
   (position-area). Sans ancrage CSS, le script la place lui-même. */
.popover-bloom-box {
  position: fixed;
  inset: auto;
  margin: 0 0 10px;
  position-area: top;
  border: 0;
  overflow: visible;
  background: linear-gradient(135deg, #4f46e5, #7c3aed);
  color: white;
  padding: 14px 22px;
  border-radius: 12px;
  font-family: sans-serif;
  font-size: 0.85rem;
  font-weight: 500;
  white-space: nowrap;
  box-shadow: 0 10px 40px rgba(99, 102, 241, 0.4);
  opacity: 0;
  scale: 0.6;
  pointer-events: none;
  transform-origin: bottom center;
  transition: opacity 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
    scale 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
    display 0.4s allow-discrete,
    overlay 0.4s allow-discrete;
}

.popover-bloom-box::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 8px solid transparent;
  border-top-color: #7c3aed;
}

/* Ouverte : état d'arrivée. @starting-style donne l'état de départ de l'entrée (le popover
   passe de display: none à affiché, il n'a pas d'état « avant » sans cette règle). */
.popover-bloom-box:popover-open {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
}

/* Repli des navigateurs sans API Popover (règle séparée : un sélecteur :popover-open inconnu
   invaliderait toute la liste). */
.popover-bloom-box.popover-bloom-open {
  opacity: 1;
  scale: 1;
  pointer-events: auto;
}

@starting-style {
  .popover-bloom-box:popover-open {
    opacity: 0;
    scale: 0.6;
  }
}

@media (prefers-reduced-motion: reduce) {
  .popover-bloom-box {
    scale: 1;
    transition: opacity 0.15s ease, display 0.15s allow-discrete, overlay 0.15s allow-discrete;
  }

  @starting-style {
    .popover-bloom-box:popover-open {
      scale: 1;
    }
  }
}
JavaScript
(function() {
  /* Popover Bloom : l'attribut popovertarget ouvre et ferme la bulle sans script (Échap et clic
     à l'extérieur compris). Ce script ajoute aria-expanded, ancre chaque bulle à son bouton et
     prend le relais là où le navigateur ne connaît pas l'ancrage CSS ou l'API Popover. */
  var hasPopover = HTMLElement.prototype.hasOwnProperty('popover');
  var hasAnchor = !!(window.CSS && CSS.supports('position-area', 'top'));
  var count = 0;

  document.querySelectorAll('.popover-bloom-box').forEach(function(box) {
    var btn = box.id && document.querySelector('[popovertarget="' + box.id + '"]');
    if (!btn) return;
    btn.setAttribute('aria-expanded', 'false');

    function place() {
      var r = btn.getBoundingClientRect();
      box.style.left = (r.left + r.width / 2) + 'px';
      box.style.top = (r.top - 10) + 'px';
      box.style.translate = '-50% -100%';
      box.style.margin = '0';
    }

    if (!hasPopover) {
      /* Navigateur sans API Popover : bascule par classe, Échap ferme. */
      box.setAttribute('aria-hidden', 'true');
      var setOpen = function(open) {
        if (open) place();
        box.classList.toggle('popover-bloom-open', open);
        box.setAttribute('aria-hidden', String(!open));
        btn.setAttribute('aria-expanded', String(open));
      };
      btn.addEventListener('click', function() {
        setOpen(!box.classList.contains('popover-bloom-open'));
      });
      document.addEventListener('keydown', function(e) {
        if (e.key === 'Escape') setOpen(false);
      });
      return;
    }

    if (hasAnchor) {
      /* Nom d'ancre propre à chaque bulle : plusieurs bulles cohabitent dans la page. */
      var name = '--popover-bloom-' + (++count);
      btn.style.setProperty('anchor-name', name);
      box.style.setProperty('position-anchor', name);
    }

    box.addEventListener('beforetoggle', function(e) {
      if (e.newState === 'open' && !hasAnchor) place();
    });
    box.addEventListener('toggle', function(e) {
      btn.setAttribute('aria-expanded', String(e.newState === 'open'));
    });
    if (!hasAnchor) {
      var follow = function() {
        if (box.matches(':popover-open')) place();
      };
      window.addEventListener('scroll', follow, true);
      window.addEventListener('resize', follow);
    }
  });
})();

Rideau de scène

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

Dialog Curtain

Fenêtre de dialogue en rideaux de scène : les rideaux se rejoignent au centre, puis le contenu apparaît.

CSS Dialog Transition
Page de l’effet →
Voir le code source
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: #4f46e5;
  color: white;
  transition: background 0.2s, transform 0.1s;
}

.dialog-curtain-btn:hover {
  background: #4338ca;
}

.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: #4f46e5;
  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
(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');
    });
  });
})();

Toast Cascade

Notifications toast empilées avec entrée fluide, reordonnancement et sortie gracieuse.

JS Animation Stack

Card Vanish

La carte se dissout en particules au retrait et se matérialise depuis les particules à l'entrée.

JS Particles Keyframes

List Item Slide

Les éléments glissent depuis le côté avec délais échelonnés et se replient à la suppression.

CSS Stagger Collapse

Accordion Fold

Sections de contenu qui se plient/déplient comme du papier avec transformations 3D perspective.

CSS 3D Perspective

Morph Transition

L'élément morphe de forme/position en passant de display:none a display:block avec allow-discrete.

CSS allow-discrete Morph

Portal Open

Le contenu apparait à travers un masque circulaire en expansion, comme un portail qui s'ouvre.

CSS clip-path Circle

Flip Card Entry

La carte entre de profil par une rotation 3D, puis sort en continuant de tourner dans le même sens.

CSS 3D Rotation Backface

Fade Through Color

Les éléments passent par une couleur de marque (pas juste l'opacité) lors de l'entrée/sortie.

CSS JS Color Transition

Motion Design

Staggered Reveal

Éléments apparaissant en cascade avec un délai progressif pour un effet coordonne.

CSS JS Cascade
Page de l’effet →
Voir le code source
HTML
<div class="md-stagger-stage">
  <div class="md-stagger"></div>
</div>
CSS
.md-stagger-stage,
.md-stagger-stage *,
.md-stagger-stage *::before,
.md-stagger-stage *::after {
  box-sizing: border-box
}

.md-stagger-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;
}

.md-stagger {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 20px;
  flex-wrap: wrap;
}

.md-stagger-item {
  width: 40px;
  height: 40px;
  border-radius: 8px;
  background: linear-gradient(135deg, #6366f1, #a855f7);
  opacity: 0;
  transform: translateY(20px) scale(0.8);
  animation: mdStaggerIn 0.6s forwards;
}

@keyframes mdStaggerIn {
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Mouvement réduit : les éléments sont posés d'emblée, sans montée ni agrandissement */
@media (prefers-reduced-motion: reduce) {
  .md-stagger-item {
    animation: none;
    opacity: 1;
    transform: none;
  }
}
JavaScript
(function() {
  // Staggered Reveal : chaque .md-stagger de la page entre en cascade quand il
  // arrive à l'écran, puis reste affiché. Il se rejoue s'il sort de l'écran et y revient.
  var once = false; // true : la cascade ne se joue qu'à la première entrée

  document.querySelectorAll('.md-stagger').forEach(function(container) {
    // Vos éléments s'il y en a dans le conteneur, sinon 12 carrés de démonstration
    var own = container.children.length > 0;

    function play() {
      if (!own) {
        container.innerHTML = '';
        for (var i = 0; i < 12; i++) container.appendChild(document.createElement('div'));
      }
      Array.prototype.forEach.call(container.children, function(item, i) {
        item.classList.remove('md-stagger-item');
        void item.offsetWidth; // relance l'animation
        item.classList.add('md-stagger-item');
        item.style.animationDelay = (i * 0.1) + 's';
      });
    }

    if (!('IntersectionObserver' in window)) {
      play();
      return;
    }
    var obs = new IntersectionObserver(function(entries) {
      if (!entries[0].isIntersecting) return;
      play();
      if (once) obs.disconnect();
    }, {
      threshold: 0.3
    });
    obs.observe(container);
  });
})();

Elastic Spring

Animation ressort avec rebond physique et déformation élastique réaliste.

CSS Physique Rebond
Page de l’effet →
Voir le code source
HTML
<div class="md-spring-stage">
  <div class="md-spring">
    <div class="md-spring-ball"></div>
  </div>
</div>
CSS
.md-spring-stage,
.md-spring-stage *,
.md-spring-stage *::before,
.md-spring-stage *::after {
  box-sizing: border-box
}

.md-spring-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;
}

.md-spring {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.md-spring-ball {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  background: linear-gradient(135deg, #6366f1, #ec4899);
  animation: mdSpring 2s cubic-bezier(0.34, 1.56, 0.64, 1) infinite;
}

@keyframes mdSpring {

  0%,
  100% {
    transform: translateY(0) scaleX(1) scaleY(1);
  }

  20% {
    transform: translateY(-60px) scaleX(0.9) scaleY(1.1);
  }

  40% {
    transform: translateY(0) scaleX(1.15) scaleY(0.85);
  }

  55% {
    transform: translateY(-25px) scaleX(0.95) scaleY(1.05);
  }

  70% {
    transform: translateY(0) scaleX(1.05) scaleY(0.95);
  }

  85% {
    transform: translateY(-8px);
  }
}

/* Mouvement réduit : ni saut ni déformation, une pulsation d'opacité garde le signal */
@media (prefers-reduced-motion: reduce) {
  .md-spring-ball {
    animation: mdSpringFade 2s ease-in-out infinite;
  }
}

@keyframes mdSpringFade {

  0%,
  100% {
    opacity: 1;
  }

  50% {
    opacity: 0.45;
  }
}
JavaScript
/* CSS pur — aucun JavaScript requis */

Orchestrated Motion

Huit barres qui s'étirent tour à tour, décalées de 0,1 s chacune, pour une vague continue en CSS pur.

CSS Keyframes Orchestré

Kinetic Wave

Propagation d'onde à travers une grille de points avec délai progressif.

CSS JS Vague

Inertia Momentum

Mouvement avec inertie et décélération naturelle : glissez le cube et regardez-le ralentir.

JS Physique Drag
Page de l’effet →
Voir le code source
HTML
<div class="md-inertia-stage">
  <div class="md-inertia">
    <div class="md-inertia-box"></div>
  </div>
</div>
CSS
.md-inertia-stage,
.md-inertia-stage *,
.md-inertia-stage *::before,
.md-inertia-stage *::after {
  box-sizing: border-box
}

.md-inertia-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;
}

.md-inertia {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
}

/* Centré par le CSS (et non mesuré au chargement) : juste même si le bloc est masqué au départ.
   touch-action: none laisse le doigt saisir le cube au lieu de faire défiler la page. */
.md-inertia-box {
  width: 50px;
  height: 50px;
  border-radius: 10px;
  background: linear-gradient(135deg, #6366f1, #06b6d4);
  cursor: grab;
  position: absolute;
  left: calc(50% - 25px);
  top: calc(50% - 25px);
  touch-action: none;
  user-select: none;
  transition: none;
}
JavaScript
(function() {
  // Inertia Momentum : saisir, lancer, l'élément glisse puis ralentit (souris, doigt, stylet)
  var friction = 0.95;
  var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');

  document.querySelectorAll('.md-inertia').forEach(function(container) {
    var box = container.querySelector('.md-inertia-box');
    if (!box) return;

    var x = 0,
      y = 0,
      vx = 0,
      vy = 0;
    var isDragging = false,
      lastX = 0,
      lastY = 0,
      raf = 0;

    function place() {
      box.style.left = x + 'px';
      box.style.top = y + 'px';
    }

    box.addEventListener('pointerdown', function(e) {
      isDragging = true;
      cancelAnimationFrame(raf);
      x = box.offsetLeft; // position réelle au moment de la saisie
      y = box.offsetTop;
      lastX = e.clientX;
      lastY = e.clientY;
      vx = 0;
      vy = 0;
      try { box.setPointerCapture(e.pointerId); } catch (err) {}
      box.style.cursor = 'grabbing';
      e.preventDefault();
    });

    box.addEventListener('pointermove', function(e) {
      if (!isDragging) return;
      vx = e.clientX - lastX;
      vy = e.clientY - lastY;
      x += vx;
      y += vy;
      place();
      lastX = e.clientX;
      lastY = e.clientY;
    });

    function release() {
      if (!isDragging) return;
      isDragging = false;
      box.style.cursor = 'grab';
      if (reduceMotion.matches) { // mouvement réduit : pas de glissade, l'élément est seulement ramené dans la piste
        x = Math.max(0, Math.min(container.clientWidth - box.offsetWidth, x));
        y = Math.max(0, Math.min(container.clientHeight - box.offsetHeight, y));
        place();
        return;
      }
      raf = requestAnimationFrame(animate);
    }
    box.addEventListener('pointerup', release);
    box.addEventListener('pointercancel', release);

    function animate() {
      if (isDragging) return;
      if (Math.abs(vx) < 0.1 && Math.abs(vy) < 0.1) return;
      vx *= friction;
      vy *= friction;
      x += vx;
      y += vy;
      var maxX = container.clientWidth - box.offsetWidth;
      var maxY = container.clientHeight - box.offsetHeight;
      if (x < 0 || x > maxX) vx *= -0.5;
      if (y < 0 || y > maxY) vy *= -0.5;
      x = Math.max(0, Math.min(maxX, x));
      y = Math.max(0, Math.min(maxY, y));
      place();
      raf = requestAnimationFrame(animate);
    }
  });
})();

SVG Path Follow

Élément qui suit un chemin SVG complexe avec une animation fluide le long de la courbe.

SVG JS Path

Character Cascade

Texte avec animation caractère par caractère, chaque lettre apparait individuellement.

JS Texte Cascade
Page de l’effet →
Voir le code source
HTML
<div class="md-char-stage">
  <div class="md-char-cascade"></div>
</div>
CSS
.md-char-stage,
.md-char-stage *,
.md-char-stage *::before,
.md-char-stage *::after {
  box-sizing: border-box
}

.md-char-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;
}

.md-char-cascade {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.md-char {
  display: inline-block;
  font-size: 2rem;
  font-weight: 700;
  color: #fff;
  opacity: 0;
  transform: translateY(20px);
  animation: mdCharIn 0.4s forwards;
}

@keyframes mdCharIn {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Le mot entier, lu par les lecteurs d'écran, invisible à l'œil */
.md-char-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Mouvement réduit : les lettres sont posées d'emblée, le mot change sans mouvement */
@media (prefers-reduced-motion: reduce) {
  .md-char {
    animation: none;
    opacity: 1;
    transform: none;
  }
}
JavaScript
(function() {
  // Character Cascade : le mot monte lettre par lettre, puis cède la place au suivant toutes les 2,5 s
  document.querySelectorAll('.md-char-cascade').forEach(function(container) {
    // Les mots : un élément enfant par mot (<span>VANNES</span><span>LORIENT</span>),
    // sinon la liste de démonstration
    var words = Array.prototype.map.call(container.children, function(el) {
      return el.textContent.trim();
    }).filter(Boolean);
    if (!words.length) words = ['MOTION', 'DESIGN', 'EFFECT', 'SMOOTH'];
    var currentWord = 0;
    var intervalId = null;

    function showWord() {
      // Animations mises en pause (bouton Pause de la page, outils d'accessibilité) : on garde le mot affiché
      var first = container.querySelector('.md-char');
      if (first && getComputedStyle(first).animationPlayState === 'paused') return;
      container.innerHTML = '';
      var word = words[currentWord];
      // Le mot entier pour les lecteurs d'écran ; les lettres, masquées pour eux, sont pour l'œil
      var label = document.createElement('span');
      label.className = 'md-char-sr';
      label.textContent = word;
      container.appendChild(label);
      word.split('').forEach(function(char, i) {
        var span = document.createElement('span');
        span.className = 'md-char';
        span.setAttribute('aria-hidden', 'true');
        span.textContent = char === ' ' ? '\u00a0' : char;
        span.style.animationDelay = (i * 0.08) + 's';
        container.appendChild(span);
      });
      currentWord = (currentWord + 1) % words.length;
    }
    showWord();
    if (words.length < 2) return;

    // Le cycle ne tourne que lorsque le bloc est visible
    if ('IntersectionObserver' in window) {
      var obs = new IntersectionObserver(function(entries) {
        if (entries[0].isIntersecting) {
          if (!intervalId) intervalId = setInterval(showWord, 2500);
        } else {
          if (intervalId) {
            clearInterval(intervalId);
            intervalId = null;
          }
        }
      }, {
        threshold: 0
      });
      obs.observe(container);
    } else {
      // Repli : cycle permanent
      intervalId = setInterval(showWord, 2500);
    }
  });
})();

3D Flip Sequence

Au clic, les cartes se retournent en 3D l'une après l'autre, avec un délai entre chaque carte : un effet domino.

CSS 3D JS Flip Sequence

Shape Morph Transition

Transition fluide entre formes géométriques : cercle, blob organique, carre, et retour.

CSS Morph Formes

Timeline Choreography

Sequence timeline multi-étapes synchronisée avec des barres de progression animées.

CSS JS Timeline

View Transitions Page Reveal

Morph fluide entre une grille et sa vue détail avec l'API View Transitions native du navigateur. Fallback automatique si non supporté.

View TransitionsAPI nativePremium

Cross-Document Page Morph

Transition fluide entre deux vraies pages (multi-page) : l'élément partagé morphe d'une page à l'autre via l'API View Transitions native, sans une ligne de JavaScript. Le SPA-feel, sans le coût du SPA.

💡 Idéal pour : blog liste→article, e-commerce listing→fiche, portfolio→étude de cas, doc, tout site multi-page.

View Transitions MPA 0 JS Premium

Questions fréquentes

Quelle est la différence entre transition et animation en CSS ? +

Une transition anime le passage d'un état A à un état B (ex: hover). Une animation peut avoir plusieurs étapes (@keyframes), boucler, et se déclencher sans interaction. Utilisez transition pour les interactions, animation pour les effets autonomes.

Comment créer une page transition en CSS ? +

Utilisez la View Transitions API (document.startViewTransition()) pour des transitions entre pages. Pour des transitions au sein d'une page, combinez opacity et transform avec une classe togglable. Personnalisez les courbes avec cubic-bezier.

Quelle durée idéale pour une transition CSS ? +

Entre 200ms et 400ms pour les interactions (hover, clic). 300ms est le standard. En dessous de 150ms, l'animation est trop rapide pour être perçue. Au-dessus de 500ms, elle semble lente. Utilisez ease-out pour les entrées et ease-in pour les sorties.

Comment éviter le layout shift lors d'une transition ? +

Animez uniquement transform et opacity (pas width, height, margin). Utilisez position:absolute pour sortir l'élément du flux. Réservez l'espace avec min-height. Préférez scale() a width pour les agrandissements.

Comment enchainer plusieurs transitions CSS ? +

Utilisez transition-delay sur chaque propriété : transition: opacity 0.3s ease 0s, transform 0.3s ease 0.1s. Ou utilisez @keyframes avec des pourcentages pour un contrôle total de la séquence.

Catégories similaires