Responsive Animation Components With Framer Motion

Nicholas Cathcart

So. Animated components. Lets start with a simple set of requirements for the purposes of this post.

  • We need a React component to handle scroll-triggered animations.
  • It should behave differently at various breakpoints.
  • It needs to be easily extendable as requirements evolve.

Framer Motion makes it easy to fulfill our first requirement. We can leverage its built-in properties "initial," "whileInView," "transition," and "viewport" to handle our animations.

import { motion } from "framer-motion";
import { ReactNode } from "react";

export interface Props {
  children: ReactNode;
}

export function ScrollWrapper({children} : Props){
  return(
    <motion.div
    initial={{ opacity: 0, y: '20px' }}
    whileInView={{ opacity: 1, y: 0 }}
    transition={{ duration: 0.5 }}
    viewport={{ once: true }}
      >
      {children}
    </motion.div>
  )
};


Here, we're using Framer Motion to extend any HTML tag within a React component with motion. We define the animation behavior declaratively. The "initial" prop sets the starting position and behavior of the tag, while "whileInView" controls its final position. The "transition" prop determines the transition duration, and "viewport" set to "once: true" ensures the animation occurs only once.But what if we want our animations to behave differently based on screen size?

Enter Tailwind CSS.

import { motion } from "framer-motion";
import { ReactNode } from "react";

export interface Props {
  children: ReactNode;
}

export function ScrollWrapper({children} : Props){
  return(
    <motion.div
    initial={{
        y: 'var(--y-from-bottom, 0px)',
        x: 'var(--x-from-left, 0px)',
        opacity: 0,
    }}
    whileInView={{
        y: 0,
        x: 0,
        opacity: 1,
    }}
    transition={{ duration: 0.5 }}
    viewport={{ once: true }}
    className={
        'max-lg:[--y-from-bottom:20px] lg:[--x-from-left:-20px]'
    }
>
      {children}
    </motion.div>
  )
};

Here, we're using Tailwind CSS to create CSS variables based on screen size, with fallbacks if those variables don't exist. This setup allows our motion div to have different initial positions based on screen size.

But what if we need more options for our component, like entering from the top or right? Let's expand our component further.

import { motion } from 'framer-motion';
import { ReactNode } from 'react';

export interface Props {
  right?: boolean;
  top?: boolean;
  classNames?: string;
  children: ReactNode;
}

export default function ScrollWrapper({
  right,
  top,
  classNames,
  children,
}: Props) {
  const variants = {
    initial: {
      y: top ? 'var(--y-from-top, 0px)' : 'var(--y-from-bottom, 0px)',
      x: right ? 'var(--x-from-right, 0px)' : 'var(--x-from-left, 0px)',
      opacity: 0,
    },
    whileInView: {
      y: 0,
      x: 0,
      opacity: 1,
    },
    viewport: {
      once: true,
    },
    transition: {
      duration: 0.5,
    },
  };

  return (
    <motion.div
      variants={variants}
      initial="initial"
      whileInView="whileInView"
      viewport={variants.viewport}
      transition={variants.transition}
      className={
        classNames + 'max-lg:[--y-from-top:-20px] max-lg:[--y-from-bottom:20px] lg:[--x-from-right:20px] lg:[--x-from-left:-20px]'
      }
    >
      {children}
    </motion.div>
  );
}


By adding more CSS variables and conditionally applying them to the initial position via ternary operators, we can pass props to change the behavior of ScrollWrapper. Cool!

This approach allows for quick iteration as requirements change, and can be applied to myriad ui components. It has the added benefit on keeping animation logic centralized, so we don't have to find and replace till we are red in the face.


Shoot me a note if you want to chat about any of the tech used here, or if you have another way of implementing responsive animations you want to share!

Cheers,
NHC