Rotate In
Six tuiles à icône SVG en grille de 2 colonnes dans un bloc de 280 px : chacune part à opacity: 0, rotate(-15deg) scale(.8) et se pose (1, 0°, 1) pendant les 46 premiers pixels de son entrée par le bas — animation-timeline: view(), animation-range: entry entry 50%, 2,3 Ko de CSS, zéro JavaScript. Les deux tuiles d'une rangée se posent ensemble : les délais de 40 à 200 ms écrits pour les échelonner sont inertes sur une timeline de défilement (mesuré).
Mis à jour le
Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Scroll comprend 70 effets, dont 12 gratuits. Découvrir la catégorie →
3 exemples d'usage



Comment ça marche
2,3 Ko de CSS, 2,2 Ko de HTML, champ JavaScript vide. Le cadre .demo-preview (flex centré, min-height: 300px, position: relative, fond #0a0a0f, texte blanc, box-sizing: border-box pour tout son contenu) enferme le bloc défilant .sdc-scroll-container — 100% × 280px, overflow-y: auto, scroll-behavior: smooth, barre de 4 px par ::-webkit-scrollbar — et l'indice .sdc-scroll-hint « Scrollez ↓ » (absolu, 8 px du bas du cadre, 10,4 px, blanc à 30 %, pulsation .3 ↔ .7 en 2 s). Dans le bloc, le plateau .sb-rotate-content : 900 px de haut, padding: 40px 12px, grille de 2 colonnes, gap: 12px. Six .sb-rotate-card (padding: 16px, rayon 12 px, fond blanc à 4 %, bordure blanc à 8 %), chacune avec un svg de 32 px (stroke: #8b5cf6, épaisseur 1,5) et un h4 de 12,8 px graisse 700, blanc. Mesuré : tuiles de 614 × 93 px à 1 280 px de large, 169 × 93 à 390 ; rangées à 40, 145 et 250 px du haut du plateau ; course 620 px. Aucun id, aucune variable du site, aucune erreur console ; collé deux fois, 12 tuiles fonctionnent. Seule scorie : un style="font-family: Inter, …" en ligne sur chaque h4 — Inter n'est pas chargée, le titre tombe sur la police système.
La mécanique tient en une règle. État de base : opacity: 0; transform: rotate(-15deg) scale(.8). L'animation sbRotateIn n'a qu'une image-clé, to { opacity: 1; transform: rotate(0deg) scale(1) }, et n'est pas pilotée par le temps mais par la position : animation-timeline: view() lie sa progression à la place de la tuile dans le conteneur de défilement le plus proche — ici .sdc-scroll-container ; sans lui, la page (mesuré). animation-range: entry entry 50% borne cette progression : 0 quand le bord haut de la tuile touche le bord bas du bloc, 1 quand elle est entrée à moitié — 46,5 px pour une tuile de 93 px, en linear, avec animation-duration: auto (la plage est la durée) et animation-fill-mode: both (état de base avant, image finale après). Mesuré, plateau poussé sous le pli : 20 px après l'entrée, opacité .43, −8,55°, échelle .886 ; 40 px : .86, −2,1°, .972 ; 46,5 px : posée. C'est une progression, pas un déclenchement : une tuile qu'on fait redescendre sous sa mi-hauteur se réincline. Les animation-delay de 40 à 200 ms posés par :nth-child(2) à (6) sont inertes : les deux tuiles d'une rangée affichent les mêmes valeurs à chaque position (.645 et .645 pour les tuiles 5 et 6 au repos) — un délai en millisecondes n'a pas de sens sur une timeline graduée en pixels. Le tag « Stagger » n'est pas tenu ; l'échelonnement, c'est l'ordre des rangées.
La démo vendue est bornée par sa géométrie : trois rangées de 93 px à partir de 40 px dans un bloc de 280. Les rangées 1 et 2 sont entières au repos, donc posées ; la rangée 3 est déjà à 64,5 % de sa fenêtre (opacité .645, −5,32°, échelle .929) et finit de se poser à 17 px de défilement ; il reste 603 px de plateau vide. L'effet est intact, c'est la démo qui le cache (voir la FAQ). Deux détails : scroll-behavior: smooth n'agit que sur les défilements programmés (scrollTop = 620 se relit 0 et arrive en 392 ms) ; une tuile à −15° réduite à .8 tient dans sa colonne (boîte englobante de 494 px pour une tuile de 614), aucune barre horizontale n'apparaît.
Accessibilité
- prefers-reduced-motion absent du code : mesuré sous émulation, rien ne change (rangée 3 à .645 au repos, posée en 17 px, indice qui pulse). Le mouvement est piloté par le lecteur — animation déclenchée par l'interaction (WCAG 2.3.3) ; pour la neutraliser :
@media (prefers-reduced-motion: reduce) { .sb-rotate-card { animation: none; opacity: 1; transform: none } .sdc-scroll-container { scroll-behavior: auto } .sdc-scroll-hint { animation: none } }.opacityettransformsont indispensables :animation: noneseul laisse la tuile invisible et inclinée. - Clavier : mesuré, Chromium 151 et Firefox 153 donnent le focus au bloc par Tab sans
tabindex(flèche bas : 40 et 51 px ; Page suivante : 285 et 303) ; WebKit 26 non — le focus reste surbody. Ajouteztabindex="0", unaria-labelet un style:focus-visiblesur.sdc-scroll-container. Au doigt, le geste doit commencer dans le bloc. - Contraste : titre blanc sur la tuile (
#0a0a0f+ 4 % de blanc) 18,4:1 ; icône#8b5cf64,3:1 (seuil 3:1 des éléments graphiques, tenu). L'opacité descend à 0 pendant l'entrée — c'est le principe —, mais une tuile bloquée à mi-entrée en bas d'un bloc court reste pâle (la rangée 3 de la démo, à .645, jusqu'au premier cran). L'indice pulse entre 1,2:1 et 1,8:1 : décoratif, posezaria-hidden="true"dessus. - Lecteurs d'écran et repli : une tuile à
opacity: 0reste dans l'arbre d'accessibilité — les six titres sont lus dès le chargement, rien n'est caché. Sixh4sansh2/h3au-dessus cassent le plan : prenez le niveau qui suit votre titre courant. Lessvgn'ont nititleniaria-hidden: ajoutezaria-hidden="true". « Scrollez ↓ » est en français en dur. Là oùview()n'existe pas, l'animation dure 0 s etfill-mode: bothapplique l'image finale : tout est visible, à plat (mesuré dans Firefox).
Compatibilité navigateur
CSS seul : animation-timeline: view(), animation-range et animation-duration: auto — les trois arrivent ensemble dans Chrome et Edge 115 et dans Safari 26 (iOS 26). Firefox : aucune version (le module existe derrière layout.css.scroll-driven-animations.enabled, désactivé par défaut). Le reste est classique : grille, transform, @keyframes, overflow-y, scroll-behavior. Zéro dépendance, zéro JavaScript, aucune variable CSS.
Mesuré dans Firefox 153, code vendu, page vierge : CSS.supports('animation-timeline: view()') est faux, animation-duration: auto est rejeté et se calcule 0s, la timeline retombe sur l'horloge du document — une animation de 0 s avec animation-fill-mode: both applique aussitôt son image finale. Les six tuiles sont à opacity: 1, rotate(0) scale(1), 94 px de haut, dès le chargement et à toute position : le visiteur Firefox voit la grille posée, à plat, lisible, dans un bloc de 280 px qui défile sur 620 px avec la barre native ; ni rotation ni fondu. Même chose dans Safari 18 et Chrome 114. Le code ne contient pas de repli @supports : ce résultat tient au couple 0s + both. Pour le rendre explicite : @supports not (animation-timeline: view()) { .sb-rotate-card { animation: none; opacity: 1; transform: none } }. Sans le cadre .demo-preview, les titres blancs deviennent invisibles sur une page claire : gardez-le ou donnez un fond aux tuiles.
Le code
Structure HTML à coller dans votre page (le CSS + JS sont inclus avec Premium) :
<div class="demo-preview">
<div class="sdc-scroll-container">
<div class="sb-rotate-content">
<div class="sb-rotate-card">
<svg viewBox="0 0 24 24">…</svg>
<h4>…</h4>
</div>
… × 6
</div>
</div>
<span class="sdc-scroll-hint">…</span>
</div>
Le code complet HTML + CSS + JS de Rotate In, prêt à coller — et les 811 effets. Ou 299 € une fois, accès à vie.
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
Inclinaison et échelle de départ (CSS — .sb-rotate-card { transform: rotate(-15deg) scale(.8) }) |
−15° / .8 | L'image finale est rotate(0deg) scale(1) : seul le départ se règle. rotate(-6deg) scale(.92) pose discrètement, rotate(15deg) tourne dans l'autre sens, un translateY(20px) ajouté aux deux états fait monter en plus. transform-origin: 0 100% fait pivoter comme une page qu'on tourne. |
Fenêtre d'entrée (CSS — animation-range: entry entry 50%) |
de 0 à 50 % de l'entrée (46,5 px pour une tuile de 93 px) | Graduée sur la hauteur de la tuile : entry entry 100% pose la tuile une fois entière (93 px de course), entry 20% entry 70% la fait attendre avant de bouger, entry 0% cover 30% continue jusqu'à 30 % de la traversée du bloc. |
Courbe (CSS — animation-timing-function: linear) |
linear | La courbe s'applique à la progression du défilement : ease-out pose vite puis freine, cubic-bezier(.34,1.56,.64,1) dépasse 0° puis revient — un petit rebond. Seul linear suit le doigt au pixel près. |
Conteneur de défilement (CSS — .sdc-scroll-container { height: 280px }, .sb-rotate-content { height: 900px }) |
bloc de 280 px, plateau de 900 px (course 620) | view() se rattache au conteneur de défilement le plus proche : sans le bloc, la page fait office (mesuré). Passez le plateau en height: auto pour que la course suive le contenu — les 900 px vendus laissent 603 px de vide. Un ancêtre en overflow: hidden est aussi un conteneur de défilement pour view() ; préférez overflow: clip. |
Grille (CSS — grid-template-columns: repeat(2, 1fr), gap: 12px, padding: 40px 12px) |
2 colonnes, 12 px, tuiles de 614 × 93 px à 1 280 px de large | Le nombre de colonnes décide combien de tuiles se posent d'un bloc (trois colonnes, trois tuiles), pas les délais. Le padding-top fixe combien de rangées sont déjà posées au chargement : 40 px en laissent deux dans un bloc de 280. |
Tuile (CSS — .sb-rotate-card, svg, h4) |
padding 16 px, rayon 12, fond blanc 4 %, bordure 8 % ; icône 32 px #8b5cf6 (1,5) ; titre 12,8 px 700 blanc | L'icône est un SVG en ligne au trait (stroke="currentColor" dans le HTML, écrasé par le stroke du CSS) : toute icône 24 × 24 au trait s'y colle. Sur fond clair, donnez à la tuile un fond et une bordure opaques et au titre une couleur sombre — les valeurs vendues sont pensées pour #0a0a0f. Retirez le style en ligne des h4 pour hériter de votre police. |
Délais :nth-child (CSS — animation-delay: 40ms … 200ms) |
40, 80, 120, 160, 200 ms — sans effet | Mesurés inertes sous view(). Supprimez-les, ou décalez la plage de la deuxième colonne pour un vrai décalage dans la rangée : .sb-rotate-card:nth-child(2n) { animation-range: entry 15% entry 65% } — mesuré, à 40 px d'entrée la première tuile est à .43 / −8,5°, la seconde à .13 / −13° ; les deux sont posées à 80 px. |
Défilement doux et indice (CSS — scroll-behavior: smooth, .sdc-scroll-hint) |
smooth / « Scrollez ↓ » pulsant 2 s | smooth ne lisse que les positions posées par script ou par ancre (mesuré : scrollTop = 620 arrive en 392 ms) ; passez à auto si un bouton pilote le bloc. L'indice s'accroche au cadre .demo-preview : sans ce cadre, il tombe au bas de la page ; supprimez le span et sdcHintPulse avec lui. |
FAQ
height: auto sur le plateau et un padding-top qui repousse la grille sous le pli. Le voisin fx-0552 a la même géométrie et la même limite.animation-timeline: view(), animation-duration: auto, fill-mode: both, et des animation-delay par :nth-child — 60 à 300 ms chez lui, 40 à 200 ici — tout aussi inertes. Trois écarts. Le mouvement : fx-0552 anime translateY(30px) → 0 avec l'opacité (sbFadeUp), une montée ; fx-0542 anime rotate(-15deg) scale(.8) → rotate(0) scale(1), une pose. La fenêtre : entry entry 40% chez lui, entry entry 50% ici — 37 px de course contre 46,5 pour une tuile de 93 px. Le contenu : des cases de texte « Item 1 » à « Item 6 » contre des tuiles icône SVG + titre. Prenez Rotate In quand la tuile est un objet — badge, pictogramme, vignette — dont la pose doit se voir sur 120 px ; Stagger Reveal quand c'est une ligne de liste qui doit simplement arriver. Ni l'un ni l'autre n'échelonne réellement les deux éléments d'une rangée ; pour cela, décalez la plage de la deuxième colonne (voir « Personnaliser »).view() prend le conteneur de défilement le plus proche, et sans .sdc-scroll-container c'est la fenêtre. Mesuré, grille posée 600 px sous le haut d'une fenêtre de 400 px : les tuiles restent à 0 / −15° / .8 sous le pli, puis se posent en 46,5 px chacune à mesure que la page descend. Gardez la règle .sb-rotate-card et le @keyframes, supprimez le bloc et les 900 px du plateau, et vérifiez qu'aucun ancêtre n'est en overflow: hidden — il deviendrait le conteneur de référence ; overflow: clip n'a pas cet effet. Quant à la réinclinaison : l'animation est une progression liée à la position, pas un déclencheur. Quand une tuile ressort par le bas sous sa mi-hauteur, sa progression redescend et elle se réincline (mesuré au retour en haut de la démo : la rangée 3 revient à .645). C'est le comportement de view(), pas un bogue ; pour une pose définitive, il faut un script — un IntersectionObserver qui pose une classe — que cet effet n'a pas.