ScrollGratuit

Corner Morph Overlay

Un calque caché sous la scène, découpé par un clip-path circle() dont le centre ne bouge jamais (90 % / 88 %) : 21 px de rayon au repos, 522 px en fin de course, 624 px de défilement fantôme dans une zone transparente de 260 px, une seule variable --p écrite par requestAnimationFrame — et l'écran est entièrement recouvert dès 86 % du parcours.

Scrollclip-pathTransition
Scrollez ici ↓
Survolez ou cliquez la scène pour interagir

Cet effet est gratuit — la catégorie Scroll en contient 70 au total, dont 12 gratuits. Effect.Labs regroupe 811 effets vanilla. Découvrir la catégorie →

Exemples d'usage

Aperçu : en défilant, l'interface passe du thème clair au thème sombre, depuis le bouton de thème.
Scrollez ici ↓

① Démo de bascule vers le mode sombre d'une application de notes

QuandPage de présentation d'une application de prise de notes : l'interface claire (barre latérale, note ouverte) est montrée en premier ; au défilement, la version sombre de la même interface gagne l'écran depuis le bouton de thème, en bas à droite.
PourquoiL'expansion circulaire part d'un point fixe, et ce point est ici le bouton de thème : l'effet devient la métaphore de l'activation qui se propage. Les persiennes de fx-0778 révèlent uniformément sur toute la largeur, sans origine liée à un élément d'interface. Les deux calques partagent la même mise en page, donc le bord du disque ne brouille aucun texte : seule la couleur bascule.
RéglagesOrigine at 90% 88% conservée, alignée sur le centre du bouton de thème ; rayon initial ramené à 0% (au repos, rien ne trahit le calque sombre : le disque naît sous le bouton) ; calque A en #f6f7fb, calque B en #14151f ; piste à 260% pour une bascule bouclée en deux écrans de molette.
Aperçu : l'univers rouge sombre de l'antagoniste envahit la page depuis le coin inférieur droit au défilement.
Continuez à lire ↓

② Introduction d'un nouveau personnage dans un roman interactif

QuandFiction interactive : une page de récit sur fond parchemin, un choix proposé au lecteur ; l'univers de l'antagoniste (rouge sombre, braises) envahit la page depuis le coin inférieur droit au fil du défilement.
PourquoiL'origine du disque est sémantique : c'est le coin par lequel le personnage entre, et le lecteur voit son monde gagner du terrain sur le texte. Les anneaux concentriques de fx-0781 dessinent une figure symétrique au centre, sans direction ni intrusion. Le récit et la réplique de l'antagoniste occupent des zones distinctes du cadre, pour que le bord du disque ne coupe jamais une phrase.
RéglagesCalque B #3b0a0f → #7a1220 avec un halo rgba(255,120,40,.3) sur l'origine ; rayon initial ramené à 3% (une braise) ; amplitude 150% ; piste à 400% pour un envahissement lent ; indication remplacée par « Continuez à lire ».
Aperçu : l'offre Premium recouvre l'offre Standard depuis le coin inférieur droit au défilement.
Scrollez ici ↓

③ Passage à l'offre Premium sur une page tarifaire

QuandSection tarifs : la carte Standard est affichée ; en défilant jusqu'à la carte, l'offre Premium surgit du coin inférieur droit et recouvre progressivement la carte Standard — même gabarit, prix et avantages supérieurs.
PourquoiL'expansion d'un coin vers le centre donne l'impression d'un niveau qui recouvre le précédent : une hiérarchie, pas une simple transition. Corner Morph Cards (fx-0780) rapproche plusieurs cartes qui adoucissent leurs coins, sans notion de recouvrement. Les deux cartes ont exactement la même grille (nom, prix, quatre lignes, bouton) pour que chaque ligne se remplace en place.
RéglagesOrigine déplacée sur le badge Premium (at 88% 90%) ; calque B en dégradé #4c1d95 → #7c3aed avec halo doré ; amplitude 121% pour que la couverture totale coïncide avec la fin de course (aucun défilement mort) ; bouton « Passer à Premium » lisible dès p = 0,5.

Comment ça marche

La scène .cmo-scene (430 × 260 px, overflow: hidden, variable --p: 0) empile trois couches absolues. Au-dessus, .cmo-scroll (z-index: 3, overflow-y: auto) ne contient qu'un bloc vide .cmo-track haut de 340% : 884 px de contenu pour 260 px de fenêtre, soit 624 px de course. C'est un défilement fantôme : la couche est transparente, rien n'y bouge, elle ne sert qu'à produire un scrollTop. Dessous, .cmo-stage (z-index: 1, pointer-events: none, aria-hidden) porte les deux calques plein cadre .cmo-a (« Chapitre I », dégradé nuit #16162a → #101020) et .cmo-b (« Chapitre II », violet #2b1440 → #7c3aed avec un halo doré à 25 % placé en 88 % / 88 %), chacun en display: grid; place-content: center. Entre les deux, l'indication .cmo-hint « Scrollez ici ↓ » (z-index: 2) s'efface en opacity: calc(1 − var(--p) × 3) : mesuré 0,52 à p = 0,16, 0,04 à p = 0,32, 0 dès 0,333.

Le script tient en quinze lignes ES5 : getElementById('cmoScroll'), closest('.cmo-scene'), puis un écouteur scroll passif qui ne fait que réserver une image via requestAnimationFrame — un drapeau raf empêche toute réservation double (mesuré : 30 événements scroll envoyés dans la même image → une seule écriture). La fonction update() calcule p = scrollTop / (scrollHeight − clientHeight), l'arrondit à quatre décimales et l'écrit en style.setProperty('--p') sur la scène ; elle est appelée une fois à l'initialisation (p = 0). Mesuré à la molette par crans de 100 px : p avance de 0,16 par cran, la course est bouclée au 7ᵉ (624 px), huit écritures en tout. Le rapport est sans unité, donc indifférent à la taille de la scène : aucun écouteur resize n'est nécessaire. Le script vise un id unique : une seconde copie du HTML reste à p = 0 (mesuré : scrollTop 300, --p inchangé) et le document porte alors deux id="cmoScroll".

Tout le rendu tient dans une règle : .cmo-b { clip-path: circle(calc(6% + var(--p) × 140%) at 90% 88%) }. Ce n'est pas le calque du dessus qui s'efface, c'est le calque caché qui est découpé par un disque dont le centre est fixe. Un rayon en pourcentage se résout sur √(largeur² + hauteur²) / √2, 357 px pour cette scène : 21 px au repos — un disque violet déjà visible dans le coin avant tout défilement — et 522 px à p = 1. Or le coin le plus éloigné (haut-gauche) n'est qu'à 452 px du centre, soit 126,5 % : l'écran est entièrement recouvert dès p = 0,861, et les 87 derniers pixels de défilement ne changent plus rien (mesuré : image identique entre p = 0,96 et p = 1). Ce seuil ne dépend que du format, pas de la taille : 0,861 aussi en 1280 × 800 et en 1920 × 1080. Autre conséquence, cette fois de deux calques qui centrent des textes différents au même endroit : entre p ≈ 0,25 et 0,5, le bord du disque coupe les titres et « Chapitre I » se mélange à « Chapitre II » (capture à p = 0,32 : « CHAPITȽE II »). Dans une intégration, donnez aux deux calques la même mise en page — c'est ce que font les trois exemples ci-dessous — ou écartez leurs textes.

Accessibilité

  • prefers-reduced-motion absent du code — mesuré sous reduce : p et le disque évoluent exactement comme sans préférence. La révélation suit le doigt ou la molette, ce n'est pas une animation autonome (WCAG 2.3.3 vise le mouvement déclenché par l'interaction), mais un disque qui balaie toute la surface est le type de transition à grande échelle que la préférence demande d'atténuer. Repli en deux lignes : @media (prefers-reduced-motion: reduce) { .cmo-b { clip-path: none; opacity: var(--p) } } — le second calque apparaît en fondu, au même rythme de défilement, sans balayage.
  • Clavier vérifié : tabindex="0" rend la zone focalisable (Tab l'atteint), Flèche bas défile de 40 px par pression (trois pressions → p = 0,19), Fin envoie à p = 1, et :focus-visible dessine un liseré violet de 2 px à l'intérieur du cadre. Mais la région est vide pour un lecteur d'écran : role="region" et aria-label="Zone de démonstration au défilement" annoncent un cadre sans contenu, la piste est un div vide et les deux calques sont aria-hidden. Si les textes révélés comptent (une offre, un chapitre), placez-les — ou une copie masquée visuellement — dans la piste défilante ; et traduisez l'aria-label, livré en français.
  • Contrastes (titres 16,8 px, sous-titres 11,5 px) : calque A, rgb(207,207,232) sur #16162a = 11,6:1 et le sous-titre à 75 % d'opacité 7,0:1 ; calque B, blanc sur #7c3aed = 5,7:1, mais le sous-titre à 75 % tombe à 3,9:1 sur la partie la plus claire du dégradé, sous le seuil 4,5:1 pour du texte de 11,5 px : passez-le à 90 % ou foncez le violet. L'indication « Scrollez ici ↓ » fait 6,2:1 mais 9,6 px (0.6rem), trop petit pour beaucoup de lecteurs : remontez-la à 11-12 px. Le disque au repos, #7c3aed sur le fond nuit, fait 3,3:1 — suffisant pour un élément graphique.
  • Tactile et cohabitation avec la page — mesuré en émulation mobile : un geste de 250 px défile la zone (p = 0,40), l'effet marche au doigt. La zone capte la molette tant qu'elle n'est pas en bout de course, puis le défilement se transmet à la page (mesuré : un cran de plus après p = 1 fait avancer la page de 101 px) : pas de piège, mais 624 px de molette absorbés pour une scène de 260 px. Gardez la scène courte ou réduisez height: 340% ; n'ajoutez pas overscroll-behavior: contain, qui bloquerait l'utilisateur dont le pointeur repose sur le cadre.

Compatibilité navigateur

Requiert clip-path: circle() avec calc() et une variable CSS, le raccourci inset (quatre règles en dépendent), place-content, closest(), requestAnimationFrame et les écouteurs passifs. Zéro dépendance, 563 octets de JavaScript ES5. La progression est lue en JavaScript (scrollTop), pas par animation-timeline: scroll() : l'effet fonctionne donc dans Firefox, qui n'implémente pas les animations pilotées par le défilement en CSS natif — un vrai critère de choix dans cette catégorie.

Chrome 87+✓ Complet
Firefox 66+✓ Complet
Safari 14.1+✓ Complet
Edge 87+✓ Complet
Mobile iOS 14.5+✓ Complet (défilement au doigt)
Android Chrome 87+✓ Complet

Sans JavaScript, --p reste à 0 (l'attribut style du HTML vendu le fixe) : le calque A s'affiche avec le disque de 21 px dans le coin et l'indication « Scrollez ici ↓ », et le défilement ne révèle rien. Sur un navigateur qui ignore inset (Safari ≤ 14.0, Chrome ≤ 86, Firefox ≤ 65), les quatre couches absolues n'ont plus de dimensions : zone de défilement de 0 × 0, effet mort. Remplacez inset: 0px par top: 0; right: 0; bottom: 0; left: 0 pour redescendre à Chrome 55, Firefox 54 et Safari 13.1, la prise en charge de circle() non préfixé. Sans clip-path du tout (Edge 12-18), le calque B, dernier dans le DOM, recouvre le calque A dès le repos : l'état final, sans transition.

Le code

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

HTML
<div class="cmo-scene" style="--p: 0.0000;">
  <div class="cmo-scroll" id="cmoScroll" tabindex="0" role="region" aria-label="Zone de démonstration au défilement">
    <div class="cmo-track" style="height:340%">
    </div>
  </div>
  <div class="cmo-stage" aria-hidden="true">
    <div class="cmo-a">
      <h4 style="font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;">Chapitre I</h4>
      <p>Continuez à défiler…</p>
    </div>
    <div class="cmo-b">
      <h4 style="font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;">Chapitre II</h4>
      <p>…le coin a tout englouti.</p>
    </div>
  </div>
  <span class="cmo-hint">Scrollez ici ↓</span>
</div>
CSS
.cmo-scene  {
   position: relative;
   width: 100%;
   max-width: 430px;
   height: 260px;
   border-radius: 12px;
   overflow: hidden;
   border: 1px solid rgba(255, 255, 255, 0.08);
   background: rgb(13, 13, 22);
   --p: 0;
   }

.cmo-scroll  {
   position: absolute;
   inset: 0px;
   overflow-y: auto;
   z-index: 3;
   scrollbar-width: thin;
   }

.cmo-stage  {
   position: absolute;
   inset: 0px;
   z-index: 1;
   pointer-events: none;
   }

.cmo-hint  {
   position: absolute;
   right: 10px;
   bottom: 8px;
   z-index: 2;
   font-size: 0.6rem;
   letter-spacing: 0.12em;
   text-transform: uppercase;
   color: rgb(143, 143, 176);
   opacity: calc(1 - var(--p) * 3);
   pointer-events: none;
   }

.cmo-scroll:focus-visible  {
   outline: rgb(167, 139, 250) solid 2px;
   outline-offset: -2px;
   }

.cmo-track  {
   width: 100%;
   }

.cmo-a, .cmo-b  {
   position: absolute;
   inset: 0px;
   display: grid;
   place-content: center;
   text-align: center;
   gap: 4px;
   }

.cmo-a  {
   background: linear-gradient(160deg, rgb(22, 22, 42), rgb(16, 16, 32));
   color: rgb(207, 207, 232);
   }

.cmo-b  {
   background: radial-gradient(circle at 88% 88%, rgba(255, 209, 102, 0.25), transparent 50%), linear-gradient(200deg, rgb(43, 20, 64), rgb(124, 58, 237));
   color: rgb(255, 255, 255);
   clip-path: circle(calc(6% + var(--p) * 140%) at 90% 88%);
   }

.cmo-a h4, .cmo-b h4  {
   margin: 0px;
   font-size: 1.05rem;
   letter-spacing: 0.18em;
   text-transform: uppercase;
   }

.cmo-a p, .cmo-b p  {
   margin: 0px;
   font-size: 0.72rem;
   opacity: 0.75;
   }
JavaScript (fx-0779)
(function() {
  /* fx-0779 — Corner Morph Overlay : progression de scroll → variable CSS --p (rAF, passif) */
  var sc = document.getElementById('cmoScroll');
  if (!sc) return;
  var host = sc.closest('.cmo-scene');
  var raf = null;
  function update() {
    raf = null;
    var max = sc.scrollHeight - sc.clientHeight;
    var p = max > 0 ? sc.scrollTop / max : 0;
    host.style.setProperty('--p', p.toFixed(4));
  }
  sc.addEventListener('scroll', function() {
    if (!raf) raf = requestAnimationFrame(update);
  }, { passive: true });
  update();
})();

Personnaliser

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

Option / propriétéDéfautEffet
Longueur de la course (HTML — .cmo-track height: 340%) 340 % (624 px de défilement pour 260 px de fenêtre) Règle la « vitesse » : p va de 0 à 1 sur (hauteur − 100 %) de la fenêtre. 200% = 260 px de course, disque bouclé en trois crans de molette ; 500% = 1 040 px, révélation lente. C'est aussi le nombre de pixels de molette absorbés avant que la page ne reprenne la main.
Rayon au repos (CSS — circle(calc(6% + …))) 6 % (21 px) Disque déjà visible avant tout défilement, une amorce qui montre où l'effet naît. 0% = rien au repos ; 12% = 43 px. En pourcentage de √(l² + h²) / √2, il suit la taille de la scène.
Amplitude (CSS — var(--p) * 140%) 140 % (522 px à p = 1) Rayon ajouté sur toute la course. Le coin opposé est à 126,5 % : 121% fait coïncider couverture totale et fin de course ; 200% couvre l'écran dès p = 0,6 et laisse 40 % de course sans changement.
Origine (CSS — at 90% 88%) 90 % 88 % (coin bas-droit) Centre fixe du disque. at 50% 50% = iris qui s'ouvre au centre (le coin n'est plus qu'à 70,4 %, couverture dès p = 0,46) ; at 0% 0% = depuis le coin haut-gauche. Pour l'ancrer sur un bouton, reprenez les coordonnées du centre du bouton en pourcentage de la scène.
Calques (CSS — .cmo-a, .cmo-b background) nuit #16162a → #101020 / violet #2b1440 → #7c3aed + halo doré 25 % Tout contenu convient : deux versions d'une même interface, une image et sa légende, deux cartes. Le halo radial de .cmo-b est posé en 88 % / 88 %, sur l'origine : déplacez-le avec elle.
Indication (CSS — .cmo-hint opacity: calc(1 − var(--p) * 3)) × 3 (invisible dès p = 0,333) * 10 = disparaît dès le premier cran ; * 1 = s'efface sur toute la course. Texte de 9,6 px : remontez font-size à 0,7 rem au moins.
Dimensions (CSS — .cmo-scene max-width: 430px; height: 260px) 430 × 260 px Le rapport p reste valable à toute taille (pourcentages partout), et le seuil de couverture ne dépend que du format. En plein écran, remplacez la zone fantôme par le défilement de la page (cf. FAQ).
Instances multiples (JS — getElementById('cmoScroll')) une seule (id) Pour plusieurs scènes, remplacez par document.querySelectorAll('.cmo-scroll').forEach(function (sc) { … }) et retirez l'id, ou dupliquez l'IIFE avec un id par scène — c'est ce que font les exemples 2 et 3.
Déclenchement au clic (JS — à ajouter) défilement seulement --p n'est qu'une variable : scene.style.setProperty('--p', 1) au clic sur un bouton, avec transition: clip-path .6s ease sur .cmo-b, donne la bascule de thème animée depuis le bouton, sans défilement.

FAQ

Pourquoi la révélation est-elle terminée avant la fin du défilement ?

Parce que le rayon final dépasse la scène. À p = 1 le disque fait 6% + 140% = 146% de la référence (√(l² + h²) / √2 = 357 px), soit 522 px, alors que le point le plus éloigné du centre — le coin haut-gauche — est à 452 px, 126,5 %. Le calque B recouvre donc tout dès p = (126,5 − 6) / 140 = 0,861 : les 87 derniers pixels de la course (sur 624) ne changent rien à l'image, mesuré identique entre p = 0,96 et p = 1. Ce seuil ne dépend que du format : 0,861 aussi en 1280 × 800 et en 1920 × 1080, 0,85 en portrait 390 × 844. Pour que la couverture coïncide avec la fin de course, remplacez 140% par 121% ; pour garder un temps de repos une fois révélé, laissez tel quel — c'est un choix, pas une erreur.

Corner Morph Overlay, SVG Blinds Mask (fx-0778) ou Corner Morph Cards (fx-0780) : lequel choisir ?

Les trois partagent le même moteur — le script de quinze lignes est identique au préfixe près, et la même piste à 340 % — ; seule diffère la règle CSS qui consomme --p. fx-0778 ferme huit lamelles .sbm-slat en scaleY(clamp(0, 1 − (var(--p) × 9 − var(--k)), 1)), l'une après l'autre, sur toute la largeur : une révélation sans origine, la légende n'apparaissant qu'à partir de p = 0,75. fx-0779 découpe un seul disque dont le centre ne bouge pas : c'est le seul des trois où la révélation a un lieu — un bouton, un coin — et une direction. fx-0780 partage le nom mais ne révèle rien : il resserre le gap, redresse la rotation et arrondit le border-radius de trois cartes. Choisissez fx-0779 quand l'origine signifie quelque chose (thème, personnage, offre), fx-0778 pour dévoiler une image sans parti pris spatial, fx-0780 pour assembler des éléments.

Comment l'appliquer à la page entière plutôt qu'à un cadre de 260 px ?

Supprimez la zone fantôme (.cmo-scroll et .cmo-track) et laissez la page défiler. Placez .cmo-stage en position: sticky; top: 0; height: 100vh dans une section haute de 340vh, puis calculez p depuis la section : var r = section.getBoundingClientRect(); var p = Math.min(1, Math.max(0, −r.top / (r.height − innerHeight))), dans un écouteur scroll passif sur window avec le même garde-fou requestAnimationFrame, et écrivez --p sur la section. Les pourcentages de circle() se résolvent alors sur la fenêtre : couverture à p = 0,861 en 16:10 comme en 16:9. Le sticky rend la main à la section suivante à la fin de la course, ce qu'un position: fixed ne ferait pas. C'est la version JavaScript de ce que animation-timeline: view() écrirait en CSS pur — avec l'avantage de fonctionner dans Firefox.