ScrollGratuit

Corner Morph Cards

Trois cartes de 86 × 116 px, penchées de −9°, 5° et 11°, aux coins dépareillés (50 / 14 / 46 / 18 px), qui se redressent, s'arrondissent uniformément et se rapprochent — l'écart passe de 34 à 12 px — au rythme d'une seule variable --p lue sur 624 px de défilement fantôme. Quatre propriétés animées par calc(), 561 octets de JavaScript, et une convergence mesurée à 20 px par carte.

Scrollborder-radiusCards
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, quatre cartes de profil éparpillées se redressent et se resserrent en un seul groupe. Équipe : Léa Marchand, direction artistique ; Sami Benali, développement ; Inès Roux, stratégie ; Tom Keller, production.
Scrollez ici ↓

① Composition d'une équipe projet au scroll

QuandSection « Notre équipe » d'une agence digitale : quatre profils (direction artistique, développement, stratégie, production) affichés très écartés, chacun penché à sa façon ; au défilement, ils se redressent et se resserrent en un seul groupe, et la légende passe de « Quatre indépendants » à « Une seule équipe ».
PourquoiLe rapprochement de plusieurs éléments distincts est la métaphore visuelle de la cohésion — c'est le seul morph du lot qui manipule un groupe d'objets qui convergent : Corner Morph Overlay (fx-0779) et Concentric Rings Morph (fx-0781) opèrent sur un seul calque ou une seule figure. Quatre cartes qui se redressent en même temps racontent « des individus qui font équipe » sans un mot.
RéglagesQuatre cartes de 84 × 108 px (avatar, nom, rôle) ; écart calc(52px − var(--p) × 44px) (52 → 8 px : 66 px de trajet par carte extérieure, contre 20 dans le code vendu) ; inclinaisons −12deg / 6deg / −5deg / 10deg ; légendes en opacity: calc(1 − var(--p) × 2) et calc(var(--p) × 2 − 1) ; noms et rôles repris dans la piste pour les lecteurs d'écran.
Aperçu : en défilant, quatre vignettes d'ingrédients éparpillées — huile d'olive, ail, tomates, basilic — se rangent et composent le plat : spaghetti al pomodoro.
Scrollez ici ↓

② Assemblage visuel d'ingrédients d'une recette signature

QuandPage d'une recette gastronomique : les quatre ingrédients (huile d'olive, ail, tomates, basilic) sont d'abord présentés éparpillés, chacun sur sa vignette penchée ; au défilement, ils se rangent côte à côte et forment la composition du plat, dont le nom apparaît.
PourquoiLa convergence traduit l'assemblage culinaire : ce sont les éléments déjà visibles qui se déplacent pour se composer. Les persiennes de SVG Blinds Mask (fx-0778) sont un masque de révélation — elles dévoilent une image, elles ne déplacent rien. Ici rien n'est caché : les ingrédients existent dès le repos, et le geste de défilement les réunit.
RéglagesFond parchemin #f7efe2, quatre vignettes de 84 × 104 px aux teintes claires (or, ivoire, tomate, basilic) avec un texte sombre pour garder 4,5:1 ; écart calc(56px − var(--p) × 48px) ; inclinaisons −8deg / 6deg / −10deg / 7deg ; nom du plat en opacity: calc(var(--p) × 2 − 1), visible à partir de p = 0,5.
Aperçu : en défilant, quatre jauges de capteurs séparées — température 62 °C, pression 4,2 bar, débit 18 m³/h, vibration 2,1 mm/s — se resserrent en un seul bloc de supervision.
Scrollez ici ↓

③ Agrégation de flux de capteurs IoT dans un tableau de bord

QuandSupervision industrielle : quatre jauges de capteurs distants (température, pression, débit, vibration) s'affichent séparées, chacune sous son propre cadre ; au défilement, elles se resserrent et s'alignent en un seul bloc de supervision, coins presque droits et 2 px d'écart.
PourquoiLe rapprochement symbolise la fusion de données disparates en une vue consolidée — Scroll Progress Beam (fx-0793) indique où l'on est dans la page, il ne réorganise pas des éléments de données entre eux. Quatre cadres qui se soudent en un tableau disent « données consolidées » mieux qu'un titre.
RéglagesQuatre jauges de 88 × 108 px ; écart calc(52px − var(--p) × 50px) (2 px en fin de course) ; coins réécrits calc(36px − p × 32px) calc(12px − p × 8px) calc(32px − p × 28px) calc(16px − p × 12px) pour finir à 4 px ; inclinaisons −7deg / 4deg / −4deg / 8deg ; en-tête « 4 flux distants » → « 1 vue consolidée ».

Comment ça marche

La scène .cmc-scene (430 × 260 px, overflow: hidden, fond rgb(13,13,22), variable --p: 0) superpose deux couches absolues. Au-dessus, .cmc-scroll (z-index: 3, overflow-y: auto, tabindex="0") ne contient qu'un bloc vide .cmc-track haut de 340% : 884 px de contenu pour 260 px de fenêtre, soit 624 px de course — un défilement fantôme, transparent, qui n'existe que pour produire un scrollTop. Dessous, .cmc-stage (z-index: 1, pointer-events: none, aria-hidden) porte une rangée flex centrée .cmc-row et ses trois .cmc-card de 86 × 116 px : chacune reçoit en ligne son inclinaison --r (−9°, 5°, 11°) et son dégradé --g (vert #22ffaa → #0e7490, violet #7c3aed → #db2777, or #ffd166 → #f4a300), un numéro en 17,6 px gras à 90 % de blanc et une ombre 0 10px 24px rgba(0,0,0,.45). Entre les deux, l'indication .cmc-hint « Scrollez ici ↓ » (9,6 px, z-index: 2) s'efface en opacity: calc(1 − var(--p) × 3) : mesuré 0,52 à p = 0,16, 0,04 à 0,32, 0 dès 0,333.

Le script (561 octets, ES5) est le moteur commun aux morphs de cette catégorie, identique à celui de Corner Morph Overlay (fx-0779) au préfixe près : getElementById('cmcScroll'), closest('.cmc-scene'), puis un écouteur scroll passif qui ne fait que réserver une image via requestAnimationFrame — le drapeau raf interdit toute réservation double (mesuré : 30 événements scroll envoyés dans la même image → une seule écriture). update() calcule p = scrollTop / (scrollHeight − clientHeight), l'arrondit à quatre décimales et l'écrit en style.setProperty('--p') sur la scène ; un appel initial pose p = 0. Mesuré à la molette par crans de 100 px : p avance de 0,1603 par cran, la course est bouclée au 7ᵉ, huit écritures en tout ; en butée, le cran suivant fait défiler la page (101 px mesurés) — pas de piège. Le rapport est sans unité, donc indifférent à la taille : 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 deux id="cmcScroll".

Tout le rendu tient dans deux règles qui lisent --p dans des calc(). Sur la rangée : gap: calc(34px − var(--p) × 22px), 34 px au repos, 12 px en fin de course. Sur chaque carte : border-radius: calc(50px − p × 40px) calc(14px + p × 0px) calc(46px − p × 36px) calc(18px − p × 8px) — quatre coins dépareillés (50 / 14 / 46 / 18) qui convergent vers 10 / 14 / 10 / 10, le deuxième étant multiplié par 0px et donc immobile — et transform: rotate(calc(var(--r) × (1 − p))) translateY(calc((1 − p) × 12px)) : l'inclinaison propre à chaque carte s'annule et une levée de 12 px retombe ; comme la translation suit la rotation, elle s'exerce dans l'axe de la carte penchée (mesuré : 1,9 px de dérive horizontale pour −9°). Mesuré dans Chromium : les centres des cartes passent de 117 et 119 px d'écart à 98 px (86 + 12), soit 19 à 21 px de trajet pour chaque carte extérieure sur 624 px de défilement, et la rangée se resserre de 341 à 282 px de large. La sensation d'assemblage vient donc autant du redressement et de l'arrondi que du rapprochement, qui reste discret. Aucune transition : le rendu suit scrollTop au pixel, et c'est le défilement lui-même (molette lissée, inertie tactile) qui donne la fluidité.

Accessibilité

  • prefers-reduced-motion absent du code — mesuré sous reduce : à 300 px de défilement, p = 0,48, écart 23,4 px et rotation −4,7°, exactement comme sans préférence. Le mouvement suit la molette ou le doigt (WCAG 2.3.3 vise l'animation déclenchée par l'interaction), mais trois objets qui pivotent, se déforment et se déplacent en même temps sont le genre de mouvement que la préférence demande d'atténuer. Repli en une règle : @media (prefers-reduced-motion: reduce) { .cmc-card { transform: none; border-radius: 10px 14px 10px 10px } .cmc-row { gap: 12px } } — l'état assemblé d'emblée, sans trajet.
  • 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" — livré en français, à traduire — annoncent un cadre dont la piste est un div vide, et les cartes sont dans un bloc aria-hidden. Si les cartes portent un sens (des personnes, des ingrédients, des capteurs), écrivez leur contenu dans la piste ou juste sous la scène.
  • Contrastes mesurés (numéros de 17,6 px gras, blanc à 90 %, pixels lus sous le texte à p = 1) : carte 02 sur le violet 4,6 à 4,8:1, conforme ; carte 01 sur le vert 2,0 à 2,5:1 au centre et 1,35:1 dans le coin clair ; carte 03 sur l'or 1,6 à 1,7:1. Aucune infraction tant que les cartes restent décoratives (aria-hidden), mais dès qu'elles portent un libellé : sur l'or, #3b2a00 donne 6,7 à 9,6:1 sur toute la carte ; sur le vert, aucune couleur ne passe d'un bout à l'autre du dégradé (#0f172a : 7,3:1 au centre, 3,3:1 dans le coin sombre) — resserrez le dégradé ou posez le texte sur un bandeau sombre. L'indication « Scrollez ici ↓ » fait 6,2:1 mais 9,6 px (0.6rem) : remontez-la à 11-12 px.
  • Tactile et cohabitation avec la page — mesuré en émulation mobile (390 px) : un geste de 250 px défile la zone (p = 0,40), l'effet marche au doigt. La zone absorbe la molette jusqu'à la butée (624 px pour une scène de 260 px) puis rend la main à la page ; n'ajoutez pas overscroll-behavior: contain, qui piégerait le pointeur posé sur le cadre. Rien ne signale qu'on peut défiler, sinon l'indication de 9,6 px et, sur Windows, la barre scrollbar-width: thin au bord droit — sur macOS elle n'apparaît qu'en défilant. Gardez la scène courte ou réduisez height: 340%.

Compatibilité navigateur

Requiert le raccourci inset (trois règles en dépendent), gap en flexbox, place-items, des calc() qui multiplient une variable CSS par un angle (rotate) et par des longueurs (border-radius, gap), closest(), requestAnimationFrame et les écouteurs passifs. Zéro dépendance, 561 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 critère qui compte 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) : trois cartes penchées aux coins dépareillés, 34 px d'écart, l'indication visible, et le défilement ne change rien — un état figé mais présentable. Sur un navigateur qui ignore inset (Chrome ≤ 86, Firefox ≤ 65, Safari ≤ 14.0), les trois couches absolues n'ont plus de dimensions : zone de défilement de 0 × 0, effet mort. Remplacer inset: 0px par top: 0; right: 0; bottom: 0; left: 0 ne gagne que Chrome 84-86 et Firefox 63-65, car gap en flexbox est le seuil suivant (Chrome 84, Firefox 63, Safari 14.1) ; sans lui, les cartes se touchent à tout moment de la course, rotation et coins continuant d'animer.

Le code

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

HTML
<div class="cmc-scene" style="--p: 0.0000;">
  <div class="cmc-scroll" id="cmcScroll" tabindex="0" role="region" aria-label="Zone de démonstration au défilement">
    <div class="cmc-track" style="height:340%">
    </div>
  </div>
  <div class="cmc-stage" aria-hidden="true">
    <div class="cmc-row">
      <div class="cmc-card" style="--r:-9deg; --g:linear-gradient(145deg,#22ffaa,#0e7490)">01</div>
      <div class="cmc-card" style="--r:5deg;  --g:linear-gradient(145deg,#7c3aed,#db2777)">02</div>
      <div class="cmc-card" style="--r:11deg; --g:linear-gradient(145deg,#ffd166,#f4a300)">03</div>
    </div>
  </div>
  <span class="cmc-hint">Scrollez ici ↓</span>
</div>
CSS
.cmc-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;
   }

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

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

.cmc-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;
   }

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

.cmc-track  {
   width: 100%;
   }

.cmc-row  {
   position: absolute;
   inset: 0px;
   display: flex;
   gap: calc(34px - var(--p) * 22px);
   align-items: center;
   justify-content: center;
   }

.cmc-card  {
   width: 86px;
   height: 116px;
   display: grid;
   place-items: center;
   background: var(--g);
   color: rgba(255, 255, 255, 0.9);
   font-weight: 800;
   font-size: 1.1rem;
   border-radius: calc(50px - var(--p) * 40px) calc(14px + var(--p) * 0px) calc(46px - var(--p) * 36px) calc(18px - var(--p) * 8px);
   transform: rotate(calc(var(--r) * (1 - var(--p)))) translateY(calc((1 - var(--p)) * 12px));
   box-shadow: rgba(0, 0, 0, 0.45) 0px 10px 24px;
   }
JavaScript (fx-0780)
(function() {
  /* fx-0780 — Corner Morph Cards : progression de scroll → variable CSS --p (rAF, passif) */
  var sc = document.getElementById('cmcScroll');
  if (!sc) return;
  var host = sc.closest('.cmc-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 — .cmc-track height: 340%) 340 % (624 px de défilement pour 260 px de fenêtre) p va de 0 à 1 sur (hauteur − 100 %) de la fenêtre. 200% = 260 px de course, assemblage bouclé en trois crans de molette ; 500% = 1 040 px, assemblage lent. C'est aussi la quantité de molette absorbée avant que la page ne reprenne la main.
Écart des cartes (CSS — .cmc-row gap: calc(34px − var(--p) * 22px)) 34 → 12 px Les deux nombres fixent le trajet : chaque carte extérieure parcourt la différence (22 px) avec trois cartes, une fois et demie avec quatre. calc(90px − var(--p) * 78px) = 78 px de trajet ; vérifiez alors que 3 × 86 + 2 × écart tient dans la scène, sinon overflow: hidden coupe les cartes au repos.
Inclinaison (HTML — --r sur chaque carte) −9deg / 5deg / 11deg Angle au repos, ramené à 0 en fin de course ; l'alternance de signes fait l'éparpillement. 0deg partout : seuls coins et écart bougent. Au-delà de ±15°, les boîtes des cartes penchées s'élargissent (mesuré : 86 → 107 px de large à 11°) et peuvent se chevaucher au repos.
Coins (CSS — .cmc-card border-radius, quatre calc()) 50 / 14 / 46 / 18 px → 10 / 14 / 10 / 10 px Chaque coin a son point de départ et son amplitude. Le deuxième est multiplié par 0px et ne bouge pas — un reste : calc(14px − var(--p) * 4px) pour finir à 10 px partout. Inversez les signes pour aller du rectangle vers le galet.
Levée (CSS — translateY(calc((1 − var(--p)) * 12px))) 12 px Descente de 12 px sur la course, dans l'axe de la carte penchée puisque translateY suit rotate (1,9 px de dérive latérale mesurés pour −9°). Placez translateY avant rotate pour une levée verticale pure ; 0px pour la supprimer.
Couleurs (HTML — --g sur chaque carte) vert #22ffaa → #0e7490, violet #7c3aed → #db2777, or #ffd166 → #f4a300 Un dégradé par carte, lu par background: var(--g). Avec du texte, gardez un contraste sur toute la carte : le violet passe (4,6:1 avec du blanc), le vert et l'or non (2,0 et 1,6:1) — cf. Accessibilité.
Indication (CSS — .cmc-hint opacity: calc(1 − var(--p) * 3)) × 3 (invisible dès p = 0,333), 9,6 px * 10 = disparaît au premier cran ; * 1 = s'efface sur toute la course. Remontez font-size à 0,7 rem au moins. scrollbar-width: none sur .cmc-scroll masque aussi la barre fine, seul autre indice de défilement.
Instances multiples (JS — getElementById('cmcScroll')) une seule (id) Pour plusieurs scènes, remplacez par document.querySelectorAll('.cmc-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.

FAQ

Les cartes ne bougent que d'une vingtaine de pixels : comment obtenir un vrai assemblage ?

Parce que le seul déplacement est celui de l'écart : gap passe de 34 à 12 px, soit 22 px par intervalle. La rangée étant centrée par flexbox, la carte du milieu ne bouge pas et chacune des deux autres parcourt 22 px (mesuré 19 à 21 px, la rotation arrondissant les boîtes) — sur 624 px de défilement. Le redressement (−9°, 5°, 11° → 0) et les coins (50 → 10 px) font le reste de l'impression. Pour un vrai rapprochement, montez l'écart au repos : gap: calc(120px − var(--p) * 108px) donne 108 px de trajet par carte extérieure, 162 px avec quatre cartes (une fois et demie l'écart). Vérifiez la largeur au repos — 3 × 86 + 2 × 120 = 498 px dépassent la scène de 430 px, que overflow: hidden couperait — en élargissant la scène ou en passant l'écart en pourcentage de sa largeur (calc(14% − var(--p) * 11%)). Les trois exemples utilisent 44 à 50 px d'amplitude sur quatre cartes.

Corner Morph Cards ou Corner Morph Overlay (fx-0779) : même moteur, quelle différence ?

Le script est le même (quinze lignes, un id et un préfixe qui changent) et la piste fantôme aussi (340 %, 624 px) : dans les deux cas, le défilement ne produit qu'une variable --p. Tout se joue dans ce que le CSS en fait. fx-0779 la dépense dans une seule propriété d'un seul calque caché — clip-path: circle() — pour révéler quelque chose depuis un point ; fx-0780 la dépense dans quatre propriétés de trois éléments visibles — gap, rotate, translateY, border-radius — et ne révèle rien : tout est là dès le repos, le défilement range. Choisissez fx-0780 quand plusieurs objets doivent former un ensemble (équipe, ingrédients, capteurs), fx-0779 quand un état doit en recouvrir un autre depuis un lieu (thème, personnage, offre). Rien n'empêche de brancher les deux règles sur une même variable : un seul script, un seul défilement, les cartes s'assemblent pendant que le fond bascule.

Comment passer à quatre cartes, ou y mettre du contenu réel ?

Chaque .cmc-card est une grille centrée (display: grid; place-items: center) : remplacez le numéro par ce que vous voulez — avatar, nom, valeur, jauge — et donnez à la carte son --r et son --g en ligne, en alternant les signes des angles (−9deg / 5deg / −7deg / 11deg) pour garder l'éparpillement. Attention à la largeur au repos : quatre cartes font 4 × 86 + 3 × 34 = 446 px, 16 px de plus que la scène, et overflow: hidden rogne 8 px sur les cartes extérieures — ramenez l'écart à calc(28px − var(--p) * 16px) ou les cartes à 80 px. Pour le texte, reprenez les contrastes de la section Accessibilité (le blanc ne passe ni sur le vert ni sur l'or) ; et si le contenu a un sens, écrivez-le aussi dans la piste défilante, la seule partie de la scène qu'un lecteur d'écran atteint. Les trois exemples ci-dessus sont construits ainsi, avec quatre cartes chacun.