DEV Community

Cover image for Pixelated Image Animation with Framer Motion
Vinay Veerappaji
Vinay Veerappaji

Posted on

Pixelated Image Animation with Framer Motion

Creating a Stunning Pixelated Image Animation with Framer Motion

Ever wanted to create that mesmerizing effect where an image appears to assemble itself pixel by pixel as you scroll? Today, I'll walk you through building a captivating pixelated image animation using React and Framer Motion that will make your portfolio stand out from the crowd.

🎯 What We're Building

We're creating an animated component that:

  • Breaks an image into a grid of individual pixels
  • Scatters these pixels randomly across the screen initially
  • Assembles them into the complete image as the user scrolls
  • Provides smooth, performant animations using Framer Motion

πŸ› οΈ The Tech Stack

  • React - For component structure
  • Framer Motion - For smooth animations and scroll-based triggers
  • TypeScript - For type safety and better developer experience
  • Tailwind CSS - For styling (optional but recommended)

🎨 The Animation Breakdown

The magic happens in two main components:

1. The PixelatedImage Component

This is our main orchestrator that sets up the grid and manages the overall animation.

2. The Pixel Component

Each individual pixel that knows how to animate itself from a random position to its final location.

πŸ’» Implementation

Let's dive into the code:

// PixelatedImage.tsx
"use client";
import { motion, useTransform, MotionValue } from "framer-motion";
import { useMemo } from "react";

interface PixelProps {
  i: number;
  src: string;
  gridSize: number;
  width: number;
  height: number;
  scrollYProgress: MotionValue<number>;
}

const Pixel = ({
  i,
  src,
  gridSize,
  width,
  height,
  scrollYProgress,
}: PixelProps) => {
  const row = Math.floor(i / gridSize);
  const col = i % gridSize;

  // Calculate which part of the image this pixel should show
  const backgroundPosition = `${(col / (gridSize - 1)) * 100}% ${(row / (gridSize - 1)) * 100}%`;
  const backgroundSize = `${gridSize * 100}% ${gridSize * 100}%`;

  // Generate random initial positions (cached with useMemo)
  const [initialX, initialY] = useMemo(
    () => [
      (Math.random() - 0.5) * width * 1.5,
      (Math.random() - 0.5) * height * 1.5,
    ],
    [width, height]
  );

  // Calculate animation timing for staggered effect
  const numPixels = gridSize * gridSize;
  const pixelProgress = i / numPixels;
  const animationStart = pixelProgress * 0.8;
  const animationDuration = 0.2;

  // Create smooth transforms based on scroll progress
  const x = useTransform(
    scrollYProgress,
    [animationStart, animationStart + animationDuration],
    [initialX, 0]
  );
  const y = useTransform(
    scrollYProgress,
    [animationStart, animationStart + animationDuration],
    [initialY, 0]
  );
  const opacity = useTransform(
    scrollYProgress,
    [animationStart, animationStart + animationDuration],
    [0, 1]
  );
  const scale = useTransform(
    scrollYProgress,
    [animationStart, animationStart + animationDuration],
    [0.5, 1]
  );

  return (
    <motion.div
      className="w-full h-full"
      style={{
        backgroundImage: `url(${src})`,
        backgroundPosition,
        backgroundSize,
        x,
        y,
        opacity,
        scale,
      }}
    />
  );
};

interface PixelatedImageProps {
  src: string;
  width: number;
  height: number;
  scrollYProgress: MotionValue<number>;
}

const PixelatedImage = ({
  src,
  width,
  height,
  scrollYProgress,
}: PixelatedImageProps) => {
  const gridSize = 20; // 20x20 grid = 400 pixels
  const numPixels = gridSize * gridSize;

  return (
    <div className="relative w-[250px] h-[250px] md:w-[400px] md:h-[400px]">
      {/* Background circle for aesthetic */}
      <div className="absolute inset-0 rounded-full bg-gray-200 dark:bg-gray-800" />

      {/* Pixel grid */}
      <div
        className="relative z-10 w-full h-full grid"
        style={{
          gridTemplateColumns: `repeat(${gridSize}, 1fr)`,
          gridTemplateRows: `repeat(${gridSize}, 1fr)`,
        }}
      >
        {Array.from({ length: numPixels }).map((_, i) => (
          <Pixel
            key={i}
            i={i}
            src={src}
            gridSize={gridSize}
            width={width}
            height={height}
            scrollYProgress={scrollYProgress}
          />
        ))}
      </div>
    </div>
  );
};

export default PixelatedImage;
Enter fullscreen mode Exit fullscreen mode

πŸ” How It Works

1. Grid Setup

We create a 20x20 CSS Grid, giving us 400 individual pixels. Each pixel is a div that shows a specific portion of the source image using CSS background-position and background-size.

2. Background Positioning Magic

const backgroundPosition = `${(col / (gridSize - 1)) * 100}% ${(row / (gridSize - 1)) * 100}%`;
const backgroundSize = `${gridSize * 100}% ${gridSize * 100}%`;
Enter fullscreen mode Exit fullscreen mode

This calculates exactly which part of the image each pixel should display. Think of it like cutting a photo into a jigsaw puzzle - each piece knows exactly where it belongs.

3. Staggered Animation

const pixelProgress = i / numPixels;
const animationStart = pixelProgress * 0.8;
Enter fullscreen mode Exit fullscreen mode

Instead of all pixels animating at once, we stagger them based on their index. This creates a beautiful cascading effect where pixels assemble in sequence.

4. Smooth Transforms

Using Framer Motion's useTransform, we create smooth transitions for:

  • Position (x, y) - From random scatter to final position
  • Opacity - Fade in effect
  • Scale - Subtle zoom-in for extra polish

πŸŽͺ Usage in a Scroll-Based Layout

Here's how to integrate it into a scroll-triggered section:

const About = () => {
  const scrollRef = useRef(null);
  const { scrollYProgress } = useScroll({
    target: scrollRef,
    offset: ["start start", "end end"],
  });

  const imageScrollYProgress = useTransform(scrollYProgress, [0, 0.8], [0, 1]);

  return (
    <section ref={scrollRef} className="py-16 h-[500vh]">
      <div className="sticky top-0 container mx-auto flex items-center h-screen">
        <div className="w-1/2 flex items-center justify-center">
          <PixelatedImage
            src="/your-image.png"
            width={250}
            height={250}
            scrollYProgress={imageScrollYProgress}
          />
        </div>
        {/* Your content here */}
      </div>
    </section>
  );
};
Enter fullscreen mode Exit fullscreen mode

⚑ Performance Considerations

  1. useMemo for Random Values: We cache the random initial positions to prevent recalculation on every render.

  2. CSS Grid Over Absolute Positioning: CSS Grid is more performant than calculating absolute positions for hundreds of elements.

  3. Transform Instead of Layout Properties: Using transform for animations is hardware-accelerated and doesn't trigger layout recalculations.

  4. Reasonable Grid Size: 20x20 (400 pixels) provides good visual quality without overwhelming the browser.

🎨 Customization Ideas

Want to make it your own? Try these variations:

  • Different Grid Sizes: Smaller grids (10x10) for a more chunky pixel effect, larger grids (30x30) for smoother transitions
  • Animation Patterns: Instead of sequential, try animating from center outward or in a spiral pattern
  • Color Effects: Add color transitions or filters during the animation
  • Multiple Images: Create a slideshow effect with multiple pixelated images

πŸš€ Browser Support

This animation works great in all modern browsers that support:

  • CSS Grid (IE11+)
  • CSS Transforms (IE9+)
  • ES6 features (or with appropriate polyfills)

πŸŽ‰ Conclusion

This pixelated image animation is a perfect example of how modern web technologies can create engaging, performant visual effects. The combination of CSS Grid, Framer Motion, and clever mathematical calculations results in an animation that's both visually stunning and technically solid.

The key takeaways:

  • Break complex animations into smaller, manageable pieces
  • Use CSS features like Grid and transforms for performance
  • Leverage Framer Motion's scroll-based animations for smooth interactions
  • Cache expensive calculations to maintain 60fps

Ready to add some pixel magic to your next project? Give it a try and let me know what creative variations you come up with!


Found this helpful? Follow me for more web animation tutorials and React tips! πŸš€

πŸ”— Resources


Top comments (0)