Transitions CSS & Animations Page
Transitions de page, view transitions, thème et animations d'entrée/sortie.
Mis à jour le
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
transformetopacitypour é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.
Fade Through
La page s'efface progressivement vers le noir puis révèle la nouvelle page.
Page de l’effet →Voir le code source
<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>.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
}
}(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);
});
});
})();Circle Reveal
Un cercle s'agrandit depuis le centre pour révéler la nouvelle page.
Page de l’effet →Pixelate Dissolve
L'écran se décompose en pixels qui apparaissent et disparaissent.
Page de l’effet →Flip Page
La page se retourne comme dans un livre.
Zoom Through
Zoom vers le centre de l'écran puis zoom arrière sur la nouvelle page.
Page de l’effet →Blinds Effect
Des stores horizontaux se ferment puis s'ouvrent pour révéler la nouvelle page.
Page de l’effet →Bubble Morph
Une forme se transforme et s'agrandit pour couvrir l'écran puis rétrécit.
Crossfade Transition
Fondu enchaine fluide entre deux états. Simule le crossfade natif de la View Transitions API.
Page de l’effet →Voir le code source
<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">★</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">♥</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>.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
}
}(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.
Page de l’effet →Scale Morph
L'élément change de taille, de forme et de couleur avec une courbe d'animation élastique.
Page de l’effet →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.
Page de l’effet →Flip Transition
La carte effectue une rotation 3D pour révéler un contenu different au verso. Effet classique et élégant.
Page de l’effet →Zoom Transition
Effet de zoom avant qui agrandit la vue pour passer à un nouvel état détaillé. Parfait pour les galeries.
Page de l’effet →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.
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.
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.
Page de l’effet →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.
Page de l’effet →Voir le code source
<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>.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;
}
}(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.
Wipe Transition
Balayage horizontal avec clip-path: inset() et ligne de séparation lumineuse qui traverse l'écran.
Page de l’effet →CSS Variables
Transition de thème pilotée par six CSS custom properties : les couleurs qui les lisent changent en douceur.
Page de l’effet →Icon Toggle
Bouton de thème avec morphing rotatif entre les icônes soleil et lune. Rotation et scale combinés.
Page de l’effet →Card Theme
Carte complète avec avatar, texte et boutons dont chaque élément transitionne entre les deux thèmes.
Page de l’effet →Gradient Shift
Fond gradient qui passe de teintes froides nocturnes à des tons chauds dorés en 0.8s.
Page de l’effet →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.
Split Compare
Les deux thèmes superposés et un séparateur « VS » qu'on fait glisser (souris, doigt, clavier) pour comparer.
Page de l’effet →Pixel Transition
Transition pixelisée : des blocs colorés apparaissent en cascade aléatoire pour révéler le nouveau thème.
Popover Bloom
L'élément s'ouvre en fleur depuis son point d'ancrage avec scale + opacity via @starting-style.
Page de l’effet →Voir le code source
<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>.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;
}
}
}(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);
}
});
})();Dialog Curtain
Fenêtre de dialogue en rideaux de scène : les rideaux se rejoignent au centre, puis le contenu apparaît.
Page de l’effet →Voir le code source
<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>.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;
}
}(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.
Page de l’effet →Card Vanish
La carte se dissout en particules au retrait et se matérialise depuis les particules à l'entrée.
Page de l’effet →List Item Slide
Les éléments glissent depuis le côté avec délais échelonnés et se replient à la suppression.
Page de l’effet →Accordion Fold
Sections de contenu qui se plient/déplient comme du papier avec transformations 3D perspective.
Page de l’effet →Morph Transition
L'élément morphe de forme/position en passant de display:none a display:block avec allow-discrete.
Page de l’effet →Portal Open
Le contenu apparait à travers un masque circulaire en expansion, comme un portail qui s'ouvre.
Page de l’effet →Flip Card Entry
La carte entre de profil par une rotation 3D, puis sort en continuant de tourner dans le même sens.
Page de l’effet →Fade Through Color
Les éléments passent par une couleur de marque (pas juste l'opacité) lors de l'entrée/sortie.
Motion Design
Staggered Reveal
Éléments apparaissant en cascade avec un délai progressif pour un effet coordonne.
Page de l’effet →Voir le code source
<div class="md-stagger-stage">
<div class="md-stagger"></div>
</div>.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;
}
}(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.
Page de l’effet →Voir le code source
<div class="md-spring-stage">
<div class="md-spring">
<div class="md-spring-ball"></div>
</div>
</div>.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;
}
}/* 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.
Kinetic Wave
Propagation d'onde à travers une grille de points avec délai progressif.
Page de l’effet →Inertia Momentum
Mouvement avec inertie et décélération naturelle : glissez le cube et regardez-le ralentir.
Page de l’effet →Voir le code source
<div class="md-inertia-stage">
<div class="md-inertia">
<div class="md-inertia-box"></div>
</div>
</div>.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;
}(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.
Page de l’effet →Character Cascade
Texte avec animation caractère par caractère, chaque lettre apparait individuellement.
Page de l’effet →Voir le code source
<div class="md-char-stage">
<div class="md-char-cascade"></div>
</div>.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;
}
}(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.
Shape Morph Transition
Transition fluide entre formes géométriques : cercle, blob organique, carre, et retour.
Page de l’effet →Timeline Choreography
Sequence timeline multi-étapes synchronisée avec des barres de progression animées.
Page de l’effet →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é.
Page de l’effet →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.
Page de l’effet →