conceptualMay 22, 2025

CSS Fundamentals for Framer Motion

Learn CSS animation concepts and how to implement them using both Tailwind CSS and Framer Motion. A practical guide for web developers.

framer-motioncssanimationstailwindweb-development

Animations are a crucial part of modern web development, enhancing user experience and making interfaces more intuitive. In this guide, we'll explore fundamental animation concepts and implement them using two popular approaches:

  1. Tailwind CSS - For quick, utility-first animations
  2. Framer Motion - For more complex, declarative animations

By understanding both methods, you'll be equipped to choose the right tool for your animation needs, whether you need simple transitions or complex interactive animations.


1. Opacity

Opacity controls the transparency of an element, where 0 is completely transparent and 1 is fully opaque. This is a fundamental CSS property for creating fade effects.

With Tailwind

In Tailwind CSS, we can animate opacity using the opacity and transition utilities. The transition class specifies which property to animate, the duration, and the timing function. For example, we use opacity-100 for full opacity, hover:opacity-50 for the hover state, and transition-opacity duration-700 ease-out to control the transition.

With Framer Motion

Framer Motion provides a more declarative way to handle animations. The same effect can be achieved using:

  • initial: Sets the starting state
  • animate: Defines the animation
  • whileHover: Handles hover states
  • transition: Controls the animation timing
"use client";
 
import { motion } from "framer-motion";
import { cn } from "@repo/shadverse/lib/utils";
 
const boxClasses =
  "h-20 w-20 flex items-center justify-center text-xs rounded-lg border text-white";
 
export default function Opacity() {
  return (
    <div className="flex flex-col gap-4">
      <div
        className={cn(
          boxClasses,
          "bg-blue-600 opacity-100 transition-opacity duration-700 ease-out hover:opacity-50"
        )}
      >
        With CSS
      </div>
 
      <motion.div
        className={cn(boxClasses, "bg-yellow-600")}
        initial={{ opacity: 0 }} // Start transparent
        animate={{ opacity: 1 }} // Fade to opaque
        whileHover={{ opacity: 0.5 }} // 50% opacity on hover
        transition={{ duration: 2, ease: "easeOut" }} // Match CSS timing
      >
        With Motion
      </motion.div>
    </div>
  );
}
With CSS
With Motion

2. Translate

Translation moves an element from one position to another without affecting the document flow. It's commonly used for sliding and moving elements.

With Tailwind

For translation in Tailwind, we use the translate-x-* utilities. The hover:translate-x-[50px] class moves the element on hover, and transition-transform duration-700 ease-out ensures a smooth animation. The transform class enables the transform property.

With Framer Motion

Framer Motion simplifies this with the x property and provides more control over the animation states.

"use client";
 
import { motion } from "framer-motion";
import { cn } from "@repo/shadverse/lib/utils";
 
const boxClasses =
  "h-20 w-20 flex items-center justify-center text-xs rounded-lg border text-white";
 
export default function TranslateBox() {
  return (
    <div className="flex flex-col gap-4">
      <div
        className={cn(
          boxClasses,
          "bg-blue-500 duration-700 ease-out hover:translate-x-[50px]"
        )}
      >
        With CSS
      </div>
 
      <motion.div
        className={cn(boxClasses, "bg-yellow-500")}
        initial={{ x: 0 }}
        whileHover={{ x: 50 }}
        transition={{ duration: 0.7, ease: "easeOut" }}
      >
        With Motion
      </motion.div>
    </div>
  );
}
With CSS
With Motion

3. Scale

Scaling changes the size of an element. It's useful for creating zoom effects and visual feedback.

With Tailwind

In Tailwind, scaling is achieved using the scale-* utilities. The hover:scale-150 class scales the element to 1.5x its size on hover, while transition-transform duration-300 ease-out controls the animation. The transform class is required to enable the transform property.

With Framer Motion

Framer Motion provides a more intuitive way to handle scaling with the scale property.

"use client";
 
import { motion } from "framer-motion";
import { cn } from "@repo/shadverse/lib/utils";
 
const boxClasses =
  "h-20 w-20 flex items-center justify-center text-xs rounded-lg border text-white";
 
export default function Scale() {
  return (
    <div className="flex flex-col gap-4">
      <div
        className={cn(
          boxClasses,
          "bg-blue-500 duration-300 ease-out hover:scale-150"
        )}
      >
        With CSS
      </div>
 
      <motion.div
        className={cn(boxClasses, "bg-yellow-500")}
        initial={{ scale: 1 }}
        whileHover={{ scale: 1.5 }}
        transition={{ duration: 0.3, ease: "easeOut" }}
      >
        With Motion
      </motion.div>
    </div>
  );
}
With CSS
With Motion

4. Rotate

Rotation spins an element around its transform origin. It's great for creating spinning and turning effects.

With Tailwind

Rotation in Tailwind is done using the rotate-* utilities. The hover:rotate-45 class rotates the element 45 degrees on hover. The transition-transform duration-300 ease-out class ensures a smooth rotation animation. The transform class is needed to enable the transform property.

With Framer Motion

Framer Motion simplifies rotation with the rotate property, which can be used in the animation states.

"use client";
 
import { motion } from "framer-motion";
import { cn } from "@repo/shadverse/lib/utils";
 
const boxClasses =
  "h-20 w-20 flex items-center justify-center text-xs rounded-lg border text-white";
 
export default function RotateBox() {
  return (
    <div className="flex flex-col gap-4">
      <div
        className={cn(
          boxClasses,
          "bg-blue-500 duration-300 ease-out hover:rotate-45"
        )}
      >
        With CSS
      </div>
 
      <motion.div
        className={cn(boxClasses, "bg-yellow-500")}
        initial={{ rotate: 0 }}
        whileHover={{ rotate: 45 }}
        transition={{ duration: 0.3, ease: "easeOut" }}
      >
        With Motion
      </motion.div>
    </div>
  );
}
With CSS
With Motion

5. Transform Origin

Transform origin changes the point around which transformations like rotate and scale are applied.

With Tailwind

Tailwind provides origin-* utilities to set the transform origin. The origin-top-left class sets the transform origin to the top-left corner. Combined with hover:scale-150 and transition-transform duration-300 ease-out, this creates a scaling effect that originates from the top-left corner. The transform class is required to enable the transform property.

With Framer Motion

Framer Motion allows you to set the transform origin directly in the animation properties.

"use client";
 
import { motion } from "framer-motion";
import { cn } from "@repo/shadverse/lib/utils";
 
const boxClasses =
  "h-20 w-20 flex items-center justify-center text-xs rounded-lg border text-white";
 
export default function TransformOriginBox() {
  return (
    <div className="flex flex-col gap-4">
      <div
        className={cn(
          boxClasses,
          "bg-blue-500 transition-transform duration-300 ease-out hover:scale-150 origin-top-left"
        )}
      >
        With CSS
      </div>
 
      <motion.div
        className={cn(boxClasses, "bg-yellow-500")}
        initial={{ scale: 1, originX: 0, originY: 0 }}
        whileHover={{ scale: 1.5 }}
        transition={{ duration: 0.3, ease: "easeOut" }}
      >
        With Motion
      </motion.div>
    </div>
  );
}
With CSS
With Motion

6. Keyframes

Keyframes allow for more complex animations by defining multiple steps in the animation sequence. Here we demonstrate three different animation approaches:

  1. CSS Animation - Using Tailwind's built-in animations
  2. Framer Motion (Page Load) - Animation that runs immediately when the component mounts
  3. Framer Motion (On View) - Animation that triggers when the element comes into the viewport

With Tailwind

For the CSS animation, we define a custom keyframe animation in our global CSS and apply it using Tailwind's animate-* utility. The animation moves the element from left to right using the move animation.

/* Keyframe animation for the move effect */
@keyframes move {
  to {
    transform: translateX(0);
  }
}
 
/* Animation utility class */
.animate-move {
  animation: move 0.6s ease-out forwards;
  will-change: transform;
}

In your component, you would use it like this:

<div
  className="h-20 w-20 bg-blue-500 animate-move"
  style={{ transform: "translateX(-100px)" }}
>
  With CSS
</div>

With Framer Motion

Framer Motion provides two main approaches for keyframe animations:

  • Page Load Animation: The animation starts automatically when the component mounts. The element is initially positioned off-screen (x: -100) and animates to its final position (x: 0) with a spring effect.

  • On View Animation: The animation triggers only when the element comes into the viewport. This is achieved using the useInView hook from Framer Motion, which detects when the element is visible and triggers the animation.

"use client";
 
import { motion, useAnimate, useInView } from "framer-motion";
import { cn } from "@repo/shadverse/lib/utils";
import { useEffect } from "react";
 
const boxClasses =
  "h-20 w-20 flex items-center justify-center text-xs rounded-lg border text-white text-center";
 
export default function KeyframesBox() {
  const [scope, animate] = useAnimate();
  const isInView = useInView(scope);
 
  useEffect(() => {
    if (isInView) {
      animate(scope.current, { x: 0 }, { type: "spring", duration: 1 });
    }
  }, [isInView]);
 
  return (
    <div className="flex flex-col gap-4">
      <div
        className={cn(boxClasses, "bg-blue-500 animate-move")}
        style={{ transform: "translateX(-100px)" }}
      >
        With CSS
      </div>
 
      <motion.div
        className={cn(boxClasses, "bg-yellow-500")}
        initial={{ x: -100 }}
        animate={{ x: 0 }}
        transition={{ type: "spring", duration: 2 }}
      >
        With Motion (Page Load)
      </motion.div>
 
      <motion.div
        className={cn(boxClasses, "bg-green-500")}
        ref={scope}
        initial={{ x: -100 }}
      >
        With Motion (On View)
      </motion.div>
    </div>
  );
}
With CSS
With Motion (Page Load)
With Motion (On View)

Where to from here?

Hopefully you'll feel a little less overwhelmed getting started with Framer Motion! You can explore motion.dev to dive deeper into animations and try adding some to your own websites and apps. I'll keep sharing my learnings as I continue exploring the world of web animations.

Happy animating! 🚀

Last updated on

On this page