A Pen by Kevin Gutowski on CodePen.
Created
March 2, 2026 15:15
-
-
Save moughamir/1e3a9f2fde0aacd665438a3f937262fc to your computer and use it in GitHub Desktop.
Scroll Animation with Grid (Motion)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <body> | |
| <div class="content-wrap"> | |
| <header> | |
| <h1 class="fluid">let's<br />scroll.</h1> | |
| <h2 class="fluid">Origionally from | |
| <a href="https://codepen.io/jh3y/pen/VYZwOwd" target="_blank">Jhey →</a>, converted to Motion | |
| </h2> | |
| </header> | |
| <main> | |
| <section> | |
| <div class="content"> | |
| <div class="grid"> | |
| <!-- Layer 1: Outer edges (6 images) --> | |
| <div class="layer"> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1463100099107-aa0980c362e6?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjZ8fGZhc2hpb258ZW58MHx8MHx8fDA%3D" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1556304044-0699e31c6a34?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8OTJ8fGZhc2hpb258ZW58MHx8MHx8fDA%3D" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1590330297626-d7aff25a0431?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTA3fHxmYXNoaW9ufGVufDB8fDB8fHww" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1487222477894-8943e31ef7b2?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTk1fHxmYXNoaW9ufGVufDB8fDB8fHww" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1488161628813-04466f872be2?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NzR8fG1vZGVsJTIwZmFzaGlvbiUyMHN0cmVldHxlbnwwfHwwfHx8MA%3D%3D" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1565321590372-09331b9dd1eb?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTR8fGFpciUyMGpvcmRhbnxlbnwwfHwwfHx8MA%3D%3D" alt="" /> | |
| </div> | |
| </div> | |
| <!-- Layer 2: Inner columns (6 images) --> | |
| <div class="layer"> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1531525645387-7f14be1bdbbd?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjM4fHxwcm9kdWN0fGVufDB8fDB8fHww" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1637414165749-9b3cd88b8271?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mjd8fHRlY2glMjBwcm9kdWN0fGVufDB8fDB8fHww" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1699911251220-8e0de3b5ce88?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8N3x8b25ld2hlZWx8ZW58MHx8MHx8fDA%3D" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1667483629944-6414ad0648c5?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTl8fGx1eHVyeSUyMHdhdGNofGVufDB8fDB8fHww" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://plus.unsplash.com/premium_photo-1706078438060-d76ced26d8d5?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MjF8fGNhbWVyYSUyMHBvbGFyb2lkfGVufDB8fDB8fHww" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1525385444278-b7968e7e28dc?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NDZ8fGl0ZW18ZW58MHx8MHx8fDA%3D" alt="" /> | |
| </div> | |
| </div> | |
| <!-- Layer 3: Center column top and bottom (2 images) --> | |
| <div class="layer"> | |
| <div> | |
| <img src="https://images.unsplash.com/reserve/LJIZlzHgQ7WPSh5KVTCB_Typewriter.jpg?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8aXRlbXxlbnwwfHwwfHx8MA%3D%3D" alt="" /> | |
| </div> | |
| <div> | |
| <img src="https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mnx8ZmFzaGlvbnxlbnwwfHwwfHx8MA%3D%3D" alt="" /> | |
| </div> | |
| </div> | |
| <!-- Center scaler image --> | |
| <div class="scaler"> | |
| <img src="https://assets.codepen.io/605876/model-shades.jpg?format=auto&quality=100" alt="" /> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <section> | |
| <h2 class="fluid">fin.</h2> | |
| </section> | |
| </main> | |
| </div> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { animate, scroll, stagger, cubicBezier } from 'https://cdn.jsdelivr.net/npm/motion@11.11.16/+esm'; | |
| // Check for reduced motion preference | |
| const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| if (prefersReducedMotion) { | |
| // Skip animations if user prefers reduced motion | |
| console.log('Reduced motion preference detected - skipping animations'); | |
| } else { | |
| let image = document.querySelector('.scaler img'); | |
| let firstSection = document.querySelector('main section:first-of-type'); | |
| let layers = document.querySelectorAll('.grid > .layer'); | |
| // Measure the natural size before animating | |
| const naturalWidth = image.offsetWidth; | |
| const naturalHeight = image.offsetHeight; | |
| // Get viewport dimensions in pixels | |
| const viewportWidth = window.innerWidth; | |
| const viewportHeight = window.innerHeight; | |
| // Animate image on scroll - shrink from full screen to natural size | |
| scroll( | |
| animate(image, { | |
| width: [viewportWidth, naturalWidth], | |
| height: [viewportHeight, naturalHeight] | |
| }, { | |
| width: { easing: cubicBezier(0.65, 0, 0.35, 1) }, // GSAP power2.inOut | |
| height: { easing: cubicBezier(0.42, 0, 0.58, 1) } // GSAP power1.inOut | |
| }), | |
| { | |
| target: firstSection, | |
| offset: ['start start', '80% end end'] | |
| } | |
| ); | |
| // Animate each layer with staggered timing | |
| // Different easing per layer: power1, power3, power4 | |
| const scaleEasings = [ | |
| cubicBezier(0.42, 0, 0.58, 1), // Layer 1: GSAP power1.inOut | |
| cubicBezier(0.76, 0, 0.24, 1), // Layer 2: GSAP power3.inOut | |
| cubicBezier(0.87, 0, 0.13, 1) // Layer 3: GSAP power4.inOut | |
| ]; | |
| layers.forEach((layer, index) => { | |
| // Calculate different end points for each layer | |
| const endOffset = `${1 - (index * 0.05)} end`; | |
| // fade: opacity stays 0 until 55% of scroll progress, then fades to 1 | |
| scroll( | |
| animate(layer, { | |
| opacity: [0, 0, 1] | |
| }, { | |
| offset: [0, 0.55, 1], // Hold at 0 until 55%, then animate to 1 | |
| easing: cubicBezier(0.61, 1, 0.88, 1) // GSAP sine.out | |
| }), | |
| { | |
| target: firstSection, | |
| offset: ['start start', endOffset] | |
| } | |
| ); | |
| // reveal: scale stays 0 until 30% of scroll progress, then scales to 1 | |
| scroll( | |
| animate(layer, { | |
| scale: [0, 0, 1] | |
| }, { | |
| offset: [0, 0.3, 1], // Hold at 0 until 30%, then animate to 1 | |
| easing: scaleEasings[index] // Different power curve per layer | |
| }), | |
| { | |
| target: firstSection, | |
| offset: ['start start', endOffset] | |
| } | |
| ); | |
| }); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* Step 1: Basic Grid Setup - No animations yet */ | |
| *, | |
| *::before, | |
| *::after { | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --gutter: 2rem; | |
| } | |
| @media (max-width: 600px) { | |
| :root { | |
| --gutter: 1rem; | |
| } | |
| } | |
| body { | |
| margin: 0; | |
| font-family: 'SF Pro Text', 'SF Pro Icons', 'Helvetica Neue', Helvetica, Arial, sans-serif, system-ui; | |
| background: #000; | |
| color: #fff; | |
| } | |
| .content-wrap { | |
| background: #000; | |
| overflow: clip; | |
| } | |
| /* Header */ | |
| header { | |
| min-height: 100vh; | |
| display: grid; | |
| align-content: center; | |
| max-width: calc(100% - (2 * var(--gutter))); | |
| padding-left: 48px; | |
| text-align: left; | |
| } | |
| h2 { | |
| padding-top: 12px; | |
| } | |
| a { | |
| color:white; | |
| } | |
| .fluid { | |
| font-size: clamp(4rem, 12vw, 12rem); | |
| line-height: 0.6; | |
| margin: 0; | |
| } | |
| h2.fluid { | |
| font-size: clamp(0.5rem, 2vw, 1.5rem); | |
| padding-top: 48px; | |
| } | |
| /* Section setup - Sticky spacer trick */ | |
| main section:first-of-type { | |
| min-height: 240vh; /* Extra height for scroll space */ | |
| } | |
| .content { | |
| min-height: 100vh; | |
| width: 100vw; | |
| display: flex; | |
| place-items: center; | |
| align-content: center; | |
| position: sticky; | |
| top: 0; | |
| overflow: hidden; | |
| } | |
| main section:last-of-type { | |
| min-height: 100vh; | |
| display: grid; | |
| place-items: center; | |
| } | |
| /* Grid Layout - 5 columns x 3 rows */ | |
| .grid { | |
| --offset: 0; | |
| --container-width: 1600px; | |
| --gap: clamp(10px, 7.35vw, 80px); | |
| width: var(--container-width); | |
| max-width: calc(100% - (2 * var(--gutter))); | |
| display: grid; | |
| grid-template-columns: repeat(5, 1fr); | |
| grid-template-rows: repeat(3, auto); | |
| gap: var(--gap); | |
| margin: 0 auto; | |
| align-content: center; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| translate: -50% -50%; | |
| } | |
| @media (max-width: 600px) { | |
| .grid { | |
| grid-template-columns: repeat(3, 1fr); | |
| --offset: -1; | |
| } | |
| .grid > .layer:nth-of-type(1) { | |
| display: none; | |
| } | |
| } | |
| /* Layers using subgrid */ | |
| .grid > .layer { | |
| display: grid; | |
| grid-column: 1 / -1; | |
| grid-row: 1 / -1; | |
| grid-template-columns: subgrid; | |
| grid-template-rows: subgrid; | |
| } | |
| /* Layer 1: Outer edges - odd on left column, even on right column */ | |
| .grid > .layer:nth-of-type(1) div:nth-of-type(odd) { | |
| grid-column: 1; | |
| } | |
| .grid > .layer:nth-of-type(1) div:nth-of-type(even) { | |
| grid-column: -2; | |
| } | |
| /* Layer 2: Inner columns */ | |
| .grid > .layer:nth-of-type(2) div:nth-of-type(odd) { | |
| grid-column: calc(2 + var(--offset)); | |
| } | |
| .grid > .layer:nth-of-type(2) div:nth-of-type(even) { | |
| grid-column: calc(-3 - var(--offset)); | |
| } | |
| /* Layer 3: Center column top and bottom */ | |
| .grid > .layer:nth-of-type(3) div:first-of-type { | |
| grid-column: calc(3 + var(--offset)); | |
| grid-row: 1; | |
| } | |
| .grid > .layer:nth-of-type(3) div:last-of-type { | |
| grid-column: calc(3 + var(--offset)); | |
| grid-row: -1; | |
| } | |
| .grid img { | |
| width: 100%; | |
| aspect-ratio: 4 / 5; | |
| object-fit: cover; | |
| border-radius: 1rem; | |
| } | |
| .grid .scaler { | |
| position: relative; | |
| grid-area: 2 / calc(3 + var(--offset)); | |
| } | |
| .scaler { | |
| z-index: 2; | |
| width: 100%; | |
| height: 100%; | |
| position: relative; | |
| img { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| translate: -50% -50%; | |
| object-fit: cover; | |
| border-radius: 1rem; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment