7 ottobre 2026
Crossfade e zoom tra scene “alla GTA” con solo CSS (Scroll-driven animations)
Un pattern elegante: sezioni che scorrono, layer fissi che si incrociano, transizioni sincronizzate allo scroll grazie a view-timeline e animation-range.
Ricreiamo un effetto di scroll cinematografico: sfondi full-bleed che si sovrappongono, dissolvenze incrociate tra scene, un leggero zoom “in uscita” e un velo colorato che maschera lo stacco. Il tutto con CSS, usando Scroll-driven Animations (view-timeline, animation-timeline, animation-range).
L’idea: far scorrere il documento, non gli sfondi
Ci sono effetti di scroll che sembrano “video editing”: lo sfondo resta incollato alla viewport, le scene si alternano con dissolvenze, e sopra continuano a scorrere testi e card. Il trucco più pulito è separare la semantica dello scroll (le sezioni che occupano spazio nel documento) dalla resa visiva (i layer fissi sovrapposti).
In pratica:
- il documento scorre normalmente con delle
<section>che hanno altezza piena viewport; - dentro ogni section c’è un
.layerche invece èposition: fixed, quindi resta fermo; - le
.layerdi scene diverse si sovrappongono nella stessa area (la viewport); - la timeline di animazione è guidata dall’entrata/uscita della section nello scroll, e la applichiamo al layer fisso.
Questo pattern permette di ottenere un crossfade naturale senza “bloccare” lo scroll della pagina.
Struttura HTML minimale: scene e pannelli
Conviene distinguere due tipologie di blocchi:
- Scene: sezioni che rappresentano gli sfondi/visual principali.
- Panel (opzionale): contenuti che devono scorrere “sopra” (testo, card, CTA), senza essere coinvolti nell’effetto fixed.
Esempio:
<main>
<section class="scene">
<div class="layer">
<img src="scene-1.jpg" alt="" />
</div>
</section>
<section class="panel">
<div class="panel__content">
<h2>Testo che scorre sopra</h2>
<p>…</p>
</div>
</section>
<section class="scene">
<div class="layer">
<img src="scene-2.jpg" alt="" />
</div>
</section>
</main>
Nota: se le immagini sono decorative, alt="" è corretto.
CSS base: la section prende spazio, il layer resta fisso
La base è volutamente semplice:
.scene {
min-height: 100vh;
}
.layer {
position: fixed;
inset: 0;
}
.layer > img {
width: 100%;
height: 100%;
object-fit: cover;
}
Così facendo:
- la pagina continua a scorrere perché le
.sceneesistono nel flow; - i
.layersi impilano uno sopra l’altro perché sono fixed e coprono la viewport.
A questo punto però è tutto sovrapposto: serve decidere quando ogni layer deve essere visibile.
Scroll-driven animations: collegare la timeline della section al layer
Qui entra in gioco la parte “moderna”: impostiamo una view timeline sulla section e la riutilizziamo nel layer.
.scene {
view-timeline-name: --scene;
view-timeline-axis: block;
}
.scene .layer {
animation-timeline: --scene;
}
view-timeline-namecrea una timeline agganciata a come la section entra/esce dalla viewport.animation-timelinefa sì che l’animazione del layer sia guidata da quella timeline, anche se il layer è fixed.
Questa è la chiave del pattern: l’elemento che “misura” lo scroll (section) e quello che si anima (layer) possono essere diversi.
Dissolvenza in entrata/uscita con animation-range
Invece di impazzire con un’unica animazione piena di percentuali, conviene spezzare in animazioni piccole e controllarle con animation-range.
Keyframes: scene-in e scene-out
@keyframes scene-in {
from {
opacity: 0;
visibility: hidden;
}
}
@keyframes scene-out {
to {
opacity: 0;
visibility: hidden;
}
}
Applicazione al layer
.scene .layer {
animation:
scene-in linear both,
scene-out linear both;
/* la prima animazione controlla l’entrata */
animation-range:
entry 50% entry 100%,
exit 50% exit 100%;
}
Cosa succede:
- quando la section è a metà entrata nella viewport (
entry 50%) la scena inizia a comparire; - quando la section è completamente entrata (
entry 100%) è pienamente visibile; - quando la section è a metà uscita (
exit 50%) inizia a svanire; - quando è completamente uscita (
exit 100%) è invisibile.
both (fill-mode) è utile perché mantiene lo stato coerente prima/dopo l’intervallo.
Nota accessibilità su opacity: 0
Usare solo opacity: 0 non rimuove l’elemento dall’albero accessibile e può creare focus “fantasma” se dentro ci sono elementi interattivi.
Qui i layer dovrebbero restare puramente decorativi. Se invece devi renderli interattivi, valuta:
- spostare l’interazione nei panel che scorrono;
- oppure gestire
pointer-events/inertvia JS (perchéinertnon è “animabile” in CSS in modo affidabile).
Il “subtle zoom” che dà profondità
La dissolvenza da sola funziona, ma un leggero zoom fa subito effetto “cinematografico”.
@keyframes subtle-zoom {
from { scale: 1.1; }
to { scale: 1; }
}
.scene .layer {
animation:
scene-in linear both,
scene-out linear both,
subtle-zoom linear both;
animation-range:
entry 50% entry 100%,
exit 50% exit 100%,
entry 50% exit 100%;
}
Scelta pratica: partire un po’ “più grande” (1.1) e andare verso 1 evita di scoprire bordi (full bleed sempre garantito).
Il “velo” per mascherare lo stacco tra scene
Quando due scene sono entrambe visibili durante l’incrocio, spesso serve un layer che “pulisca” la transizione: una tinta, un overlay, un flash morbido.
Un modo ordinato è usare un pseudo-elemento sul layer:
.layer::after {
content: "";
position: absolute;
inset: 0;
background: #0b3a70; /* colore del velo */
opacity: 0;
}
@keyframes scene-switch {
100% { opacity: 0.9; }
}
.scene .layer::after {
animation: scene-switch linear both;
animation-timeline: --scene;
animation-range: exit 0% exit 25%;
}
Qui il velo si alza rapidamente all’inizio dell’uscita della scena (regola animation-range a gusto). Se vuoi un effetto “doppio” (sale e poi scende), puoi anche usare keyframes più articolati:
@keyframes scene-switch {
0% { opacity: 0; }
40% { opacity: 0.9; }
100% { opacity: 0; }
}
e regolare un range che copra il tratto di incrocio.
Supporto browser e strategia di fallback
Le Scroll-driven Animations (view-timeline / animation-range) non sono ancora uniformi ovunque. Quindi:
- usale come progressive enhancement;
- prevedi un’esperienza base solida senza queste proprietà.
Esempio di gate:
@supports (view-timeline-name: --x) {
/* tutto il sistema con timeline */
}
Nel fallback puoi mantenere:
- scene statiche una sotto l’altra (niente fixed); oppure
- un layout con un solo hero e immagini normali.
In sintesi: un pattern riutilizzabile
L’effetto “scene che si incrociano” nasce da tre scelte architetturali:
- Le section creano lo spazio di scroll (
min-height: 100vh). - I layer visivi sono fixed e sovrapposti (
position: fixed; inset: 0). - L’entrata/uscita della section guida le animazioni del layer (
view-timeline+animation-timeline+animation-range).
Aggiungendo una dissolvenza pulita, un subtle zoom e un velo di transizione, ottieni un risultato molto vicino alle landing “cinematografiche”, con una quantità di codice sorprendentemente contenuta e una separazione netta tra contenuto che scorre e sfondo che racconta.