Crossfade Transition
Deux états superposés au même endroit : l'un s'efface pendant que l'autre apparaît, en 0,8 s, sur un simple basculement de classe — ni mouvement ni couleur intermédiaire, les deux images se mélangent pendant le passage. CSS + 0,9 Ko de JavaScript, clic, Entrée ou Espace, fondu raccourci à 0,15 s si l'utilisateur réduit les animations.
Mis à jour le
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
Monture Anna — écaille miel
Acétate italien poli à la main, charnières 5 barils. Taille 50 □ 20 – 145, fabriquée dans le Jura.
T3 · 64,2 m²
Bâtiment B · 3e étage · lot B12
Balcon sud 7 m² · livraison 3e trim. 2027
- Séjour 24,6 m²
- Chambre 1 11,2 m²
- Chambre 2 10,4 m²
Comment ça marche
La scène .crossfade-stage (250 px de haut, fond #1a1a24, contenu centré) porte un cadre .crossfade-frame de 280 × 180 px, coins de 16 px, overflow: hidden. Dedans, deux calques .crossfade-state en position: absolute; inset: 0 occupent exactement la même place : l'état A (dégradé indigo vers violet, opacité 1) et l'état B, posé au-dessus dans le HTML (dégradé rose vers magenta, opacité 0). Les deux ont la même règle transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1).
Tout le passage tient dans deux règles : .crossfade-frame.active .crossfade-state-a { opacity: 0 } et .crossfade-frame.active .crossfade-state-b { opacity: 1 }. Le script pose ou retire .active au clic, à Entrée ou à Espace sur chaque .crossfade-frame de la page, met à jour aria-pressed et masque aux lecteurs d'écran l'état caché (aria-hidden). L'état B reste affiché tant qu'on ne rappuie pas ; le second appui refait le fondu en sens inverse. Mesuré dans Chromium 151 et Firefox 153 : 250 ms après le clic, A est à 0,63 et B à 0,37 ; à 1,1 s, A à 0 et B à 1 ; trois secondes plus tard, toujours B ; le second clic ramène A à 1. Même résultat dans WebKit 26.5.
Le fondu n'est pas une surimpression parfaite : à mi-parcours, chaque calque est à environ 50 %, donc près d'un quart du fond de la scène transparaît (0,37 × 0,63 = 0,23 au relevé de 250 ms). Sur deux images claires, on voit un léger creux sombre au milieu du passage ; c'est ce qui rend ce fondu doux, et la FAQ dit comment le supprimer. Le code ne dépend ni de la page ni d'un id : plusieurs cadres cohabitent, chacun avec son état, et le script n'expose rien sur window. Une variante de ce fondu existe aussi dans la catégorie.
Accessibilité
- Mouvement réduit : sous
prefers-reduced-motion: reduce, la durée du fondu passe de 0,8 s à 0,15 s. Le fondu ne déplace rien, il est donc gardé, seulement raccourci (mesuré : passage terminé en moins de 260 ms dans Chromium et Firefox). - Clavier et lecteurs d'écran : le cadre porte
role="button",tabindex="0"etaria-pressed; Entrée et Espace basculent comme un clic (mesuré dans les deux moteurs). L'état caché reçoitaria-hidden="true": le nom lu du bouton est le texte de l'état visible. Un contour de 2 px apparaît au focus clavier (:focus-visible). - Ne rien cacher d'important dans le passage : pendant 0,8 s les deux textes se superposent et aucun n'est lisible. Ne mettez pas d'information qui n'existerait qu'au milieu du fondu ; les deux états doivent se suffire.
- Contraste : le texte blanc tient sur les deux dégradés de démonstration ; si vous mettez des photos, ajoutez un voile sombre derrière le texte, surtout au milieu du fondu où les deux images se mélangent.
Compatibilité navigateur
CSS (opacity, transition, inset, :focus-visible) + 0,9 Ko de JavaScript, aucune dépendance. Mesuré dans Chromium 151, Firefox 153 et WebKit 26.5 : bascule, état B tenu, retour au second clic ; clavier et mouvement réduit mesurés dans Chromium et Firefox. La propriété inset fixe le plancher (Chrome 87, Firefox 66, Safari 14.1) ; :focus-visible et le flou de l'icône (backdrop-filter) sont des finitions qui disparaissent sans casser l'effet sur un navigateur plus ancien.
Sans JavaScript, l'état A reste affiché et le cadre ne réagit pas. Sur un navigateur qui ignore inset, remplacez-le par top: 0; right: 0; bottom: 0; left: 0. Sans backdrop-filter, l'icône garde son fond blanc à 20 %, sans flou.
Le code
Copiez les trois blocs dans votre page. Aucune dépendance.
<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(); }
});
});
})();
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
Durée (CSS — transition de .crossfade-state) |
0,8 s | 0,4 s pour un changement d'onglet vif, 1,2 s pour une vitrine contemplative. Au-delà d'1,5 s, l'utilisateur reclique pendant le fondu. |
Courbe (CSS — cubic-bezier(0.4, 0, 0.2, 1)) |
démarrage doux, fin douce | linear donne un fondu régulier ; ease-out fait apparaître B plus vite au début. |
Contenu des états (HTML — .crossfade-state-a, -b) |
icône + deux lignes de texte sur un dégradé | Mettez-y n'importe quel contenu de même taille : photo, plan, carte. Plus les deux contenus partagent la même géométrie, plus le fondu montre clairement ce qui change. |
Taille du cadre (CSS — .crossfade-frame) |
280 × 180 px, coins de 16 px | width: 100%; aspect-ratio: 16/9 pour une image pleine largeur ; le cadre garde overflow: hidden pour que les coins arrondis coupent les deux états. |
Hauteur de la scène (CSS — .crossfade-stage) |
250 px, fond #1a1a24 |
Retirez la hauteur et le fond si le cadre s'insère dans votre propre mise en page ; la scène ne sert qu'à centrer la démo. |
FAQ
Pourquoi voit-on un léger creux sombre au milieu du fondu ?
Les deux calques passent par 50 % en même temps : à cet instant, le fond de la scène transparaît pour environ un quart. Si l'état B est opaque (une photo, un fond plein), gardez l'état A à opacité 1 et ne faites varier que B : .crossfade-frame.active .crossfade-state-a { opacity: 1 }. B, posé au-dessus, recouvre A en fondu sans creux. Gardez la version croisée quand les états ont de la transparence, sinon A resterait visible sous B.
Est-ce l'API View Transitions ?
Non. C'est une transition CSS d'opacité sur deux calques déjà présents dans la page, qui marche partout depuis longtemps. L'API View Transitions (document.startViewTransition) produit un fondu comparable entre deux états du DOM sans garder les deux contenus en permanence, mais demande un navigateur récent (Chrome 111, Safari 18, Firefox 144) et un repli ailleurs. Pour basculer entre deux contenus connus à l'avance, les deux calques suffisent.
Comment enchaîner plus de deux états ?
Ajoutez autant de calques .crossfade-state que d'états, tous à opacité 0, et une classe is-on qui met l'actif à 1. Au clic, retirez is-on de l'état courant et posez-la sur le suivant : la même transition fait le fondu. Gardez aria-hidden sur tous les états sauf l'actif.