Animate beyond your React-Forms with Framer-motion

Unlock the magic of animations in your React applications with Framer Motion! 🚀.How can this powerful animation library help you build engaging and fluid imaginaions??

October 17, 202410 min read

Introduction

Have you ever wondered how apps make those delightful transitions that instantly capture your attention? Whether it’s buttons gliding across the screen or elements fading in as you scroll, animations make interfaces engaging. With Framer Motion, you can easily add stunning interactions to your React apps without being an animation expert.

In this guide, we’ll walk you through creating eye-catching animations, from simple transitions to complex gestures.

What You'll Learn

By the end of this guide, you will:

  1. Understand the Basics of Framer Motion: Learn about key concepts such as the motion component, animation properties, and variants.
  2. Implement Advanced Animation Techniques: Use animation controls and gesture animations to enhance user interactions.
  3. Optimize Performance and Accessibility: Learn best practices for performance optimization and accessibility considerations.
  4. Create Custom Animation Hooks: Develop reusable animation hooks to manage complex animation logic.
  5. Integrate Error Handling and Loading States: Use Suspense and ErrorBoundary to handle loading and error states gracefully.

Try Kodaschool for free

Click below to sign up and get access to free web, android and iOs challenges.

Sign Up

Prerequisites

Basic Knowledge of React (creating custom hooks)

Nodejs and npm

Tailwindcss installed (using postcss)

What's Framer Motion?

Framer Motion is a popular open-source animation library from Framer for React that allows developers to manipulate DOM elements e.g enables them create and manage complex animations and transitions within apps.It's an excellent alternative to making complex CSS or Javascript animations.

Key Concepts

Gestures: Describes adding interactivity with gestures like drag, hover, and tap, which can trigger animations.


Variants:
Introduces reusable animation states with variants, a powerful tool for applying animations to multiple elements.


Initial and Animate States: Focuses on how animations are defined through initial and animate props, directly targeting the animation states.


Layout Animations:
Discusses seamless animations during layout changes, providing an advanced concept for dynamic UIs.


Transitions: Explains the ability to control timing and easing of animations using the transition prop.

APIs/Components in Framer motion

1. The 'motion' component

The motion component encapsulates HTML elements in React components, eg div, li, h1, ul,button ,p allowing you to animate them using initial and animate props.

<motion.div
  className="absolute top-1/4 right-1/4"
  initial={{ scale: 0 }}
  animate={{ scale: 1 }}
  transition={{ duration: 0.5, delay: 1 }}
>
  <img src={logoImage} alt="Stream Lunar Logo" />
</motion.div>

Key animation properties;

  • initial: Starting state
  • animate: End state
  • transition: How the animation plays out
  • exit: State when component unmounts

Lets Dig Deep

a) Initial prop

initial={{ opacity: 0 }}

Initial State: The initial prop defines the starting point of the animation. In this case, the motion.div will start with an opacity of 0 (completely invisible) when it first appears.

b)Aniimate prop

animate={{ opacity: 1 }}

Animation State: The animate prop defines the final state the element will animate to. Here, the motion.div will transition from its initial state (opacity 0) to an opacity of 1 (fully visible).


c) exit Prop

exit={{ opacity: 0 }}

Exit Animation: The exit prop defines how the element will animate out when it’s removed from the DOM. In this case, when the motion.div is unmounted, it will fade out (go back to opacity 0).

d)transition Prop:

transition={{ duration: 2.3 }}

Transition Duration: This controls how long the animation takes to complete. The transition prop here specifies that the animation will last 2.3 seconds, whether it's fading in or fading out.

2. AnimatePresence

AnimatePresence allows components to animate out when they're removed from the React tree/DOM.

It works hand in hand with the motion components

<AnimatePresence>
   <motion.div>
   ----......
   ---
    </motion.div>
</AnimatePresence>

3. Variantts

variants define a set of animation states that can be reused across multiple components. Each component has its own animation behavior, and these behaviors are controlled by the initial, animate, and interaction-based properties like whileHover and whileTap.

const formVariants = {
    hidden: { opacity: 0, y: 50 },
    visible: {
        opacity: 1,
        y: 0,
        transition: {
            duration: 0.6,
            staggerChildren: 0.1
        }
    }
};



The A form (motion.div) that fades in from below.


Purpose: This defines the animation states for the form as a whole.

Hidden State:

opacity: 0 makes the form invisible at first.
y: 50 shifts the form 50 pixels down, so it starts out off-screen.
Visible State:

opacity: 1 fades the form in, making it fully visible.
y: 0 moves the form back to its original position, making it appear from below.
transition:
duration: 0.6: This controls the time it takes for the animation to complete (0.6 seconds).
staggerChildren: 0.1: This delays the animation of child elements (like input fields) by 0.1 seconds, creating a smooth cascading effect.

in you JSX
<motion.div
    className="absolute top-1/4 left-52 transform -translate-x-1/2 -translate-y-1/2"
    initial="hidden"
    animate="visible"
    variants={formVariants}
>

This motion.div will fade in and move up from its starting point when the component is rendered. It will animate over 0.6 seconds, and the children (input fields) will animate in a staggered manner thanks to the staggerChildren property.

4. Gestures and AnimationHelpers

Gestures allow you to add interactive animations that respond to user input, like hovering, tapping, dragging, and scrolling.

Hover Gestures(whileHover)

This property triggers animations when the user hovers over an element.

This is commonly provides feedback, like enlarging a button or changing its appearance when a user hovers over it.

~Behavior: The element scales up (enlarges) to 1.1 times its original size when the user hovers

<motion.div
  whileHover={{ scale: 1.1 }}
>
  Hover over me!
</motion.div>

Tap Gestures (whileTap)

The whileTap property triggers an animation when the user taps or clicks on an element.

~Behavior: The element shrinks (scales down to 0.9) when the user taps or clicks on it, simulating a pressed state.

<motion.div
  whileTap={{ scale: 0.9 }}
>
  Tap me
</motion.div>

ScrollGestures {useAnimationScroll}

scroll-based animations, allows you to animate elements when they come into view as you scroll.

~Behavior: The element fades in as the user scrolls it into view,

<motion.div
  initial={{ opacity: 0 }}
  whileInView={{ opacity: 1 }}
  viewport={{ once: true }}
>
  Check my Fade effect
</motion.div>

NOTE:

You can easily combine multiple gestures in a single component to create rich interactivity.The button enlarges and changes its background color when hovered and shrinks slightly when clicked

<motion.button
  whileHover={{ scale: 1.1, backgroundColor: "#f00" }}
  whileTap={{ scale: 0.9 }}
>
  Interactive Button
</motion.button>

5.Creating and Leveraging Hooks

In the code below we are creating a react hoook named useAnimationControls that exposes the conttrols and start o ue ino ou components

import { useAnimationControls } from "framer-motion";

export const useShakeAnimation = () => {
    const controls = useAnimationControls();

    const shake = async () => {
        await controls.start({
            x: [0, 10, -10, 10, 0],
            transition: { duration: 0.4 }
        });
    };

    return { controls, shake };
};

useAnimationControls:

This hook allows for control animations programmatically e.g creating animation controls that can be used to manually start/stop animations on one or more motion components,


controls:

It's the control object returned by useAnimationControls


shake:

This function defines the shake animation. It uses the controls.start method to start the animation with the specified properties.

Invoke the shake() method:

   const handleSubmit = (e) => {
        e.preventDefault();
        
        setIsError(true);
        shake(); 
    };

Utilizing the controls object :

In your JSX:

  <motion.div
                      className="absolute top-1/4 left-52 transform -translate-x-1/2 -translate-y-1/2"
                        initial={{ opacity: 0, y: 50 }}
                        animate={isError ? { x: [0, 10, -10, 10, 0], opacity: 1, y: 0 } : { opacity: 1, y: 0 }}
                        transition={{ duration: 0.5, delay: 1.5 }}
                    >

~animate prop:

The ternary operator conditionally set the animate
property based on the IsError state. If the isError state is true, it applies a shaking animation to the x property, indicating an error.
Otherwise, it sets the opacity to 1 and y to 0, ensuring the element is fully visible and stationary, thus enhanced errors indication.

Lets try out a real-world scenario

Projec structure

Initialize your project using Vite

npm init vite@latest

Select React and Javasript

Image

Ensure that your src folder:

Find the images assets folder of this repo

📁 frontend/
├── 📁 node_modules/
├── 📁 public/
├── 📁 src/
│   ├── 📁 assets/
│   │   ├── 📁 css/
│   │   │   ├── 📄 landing.css
│   │   ├── 📄 l5-removebg-preview...
│   │   ├── 📄 large_moon.png
│   │   └── 📄 small_moon.png
│   ├── 📁 components/
│   │   └── 📄 loginxl.jsx
│   ├── 📁 hooks/
│   ├── 📁 styles/
│   ├── 📄 App.jsx
│   ├── 📄 errorboundary.jsx
│   ├── 📄 index.css
│   └── 📄 main.jsx

ERRORBOUNDARY.JSX

import  { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {

    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <h1 className='p-10 m-3 flex justify-center align-baseline'>Something went wrong.</h1>;
    }

    // eslint-disable-next-line react/prop-types
    return this.props.children;
  }
}

export default ErrorBoundary;

The code acts as a fallback ui for any erros within the react-tree.

APP.JSX

import { Routes, Route, BrowserRouter } from "react-router-dom";
import { Suspense } from "react";
import { motion } from "framer-motion";
import ErrorBoundary from "./errorboundary";
import { LoginXL } from "./components/loginxl";

const App = () => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        transition={{ duration: 2.3 }}
      >
        <ErrorBoundary>
          <BrowserRouter>
          <Routes>
         <Route path="/" element={<LoginXL />} />  
            </Routes>
          </BrowserRouter>
        </ErrorBoundary>
      </motion.div>
    </Suspense>
  );
};

export default App;

In the 'App' component we use Framer Motion to animate the opacity of the content, React Router to handle navigation, Suspense to provide a loading fallback, and ErrorBoundary to catch and handle errors.

hooks/index.jsx

import{ useAnimationControls } from 'framer-motion';
export const useShakeAnimation = () => {
    const controls = useAnimationControls();

    const shake = async () => {
        await controls.start({
            x: [0, 10, -10, 10, 0],
            transition: { duration: 0.4 }
        });
    };

    return { controls, shake };
};

useShakeAnimation Hook hook, useShakeAnimation, utilizes Framer Motion's useAnimationControls to create a reusable shake animation.

components/loginxl.jsx

import smallMoonImage from '../assets/small_moon.png';
import logoImage from '../assets/l5-removebg-preview ff 2.png';
import largeMoonImage from '../assets/large_moon.png';
import '../assets/css/landing.css';
import { motion} from 'framer-motion';
import React from 'react';
import { Link } from 'react-router-dom';
import { MdOutlineMailOutline, MdLockOutline } from 'react-icons/md';
import {
    Card,
    Input,
    Typography,
} from "@material-tailwind/react";
import { useShakeAnimation } from '../hooks';

export const LoginXL = () => {
    // Destructuring controls and shake function from the custom hook to manage shake animation
    const { controls, shake } = useShakeAnimation();
    
    // State to manage whether an error has occurred (e.g., on incorrect login)
    const [isError, setIsError] = React.useState(false);

    // Function to handle "forgot password" actions
    const handleForgotPassword = () => {
        // In a real implementation, this would handle the forgot password logic (e.g., sending a reset email)
    };

    // Function to handle form submission (e.g., user trying to sign in)
    const handleSubmit = (e) => {
        e.preventDefault(); // Prevent default form submission behavior
        
        setIsError(true);  // Setting an error state to true (simulating an error like incorrect login)
        shake();  // Triggering the shake animation when an error occurs
    };

    return (
        <div className="landing-screen large-screen-only">
            <div className="container mx-auto px-4">
                {/* Flex container to align logo and form */}
                <div className="flex flex-col md:flex-row items-center justify-center">

                    {/* Logo animation: scales from 0 to full size */}
                    <motion.div
                        className="absolute top-1/4 right-1/4 transform -translate-y-1/2 translate-x-1/2"
                        initial={{ scale: 0 }}  // Initial scale of 0 (invisible)
                        animate={{ scale: 1 }}  // Scale to 1 (full size) after animation
                        transition={{ duration: 1.5, delay: 1 }}  // Animation duration of 1.5 seconds with a delay of 1 second
                    >
                        <img src={logoImage} alt="Stream Lunar Logo" className="w-80" />
                    </motion.div>

                    {/* Small and large moon images as background elements */}
                    <img src={smallMoonImage} alt="Small Moon" className="small-moon" />
                    <img src={largeMoonImage} alt="Large Moon" className="large-moon" />

                    {/* Form card with motion and conditional shake effect on error */}
                    <motion.div
                        className="absolute top-1/4 left-52 transform -translate-x-1/2 -translate-y-1/2"
                        initial={{ opacity: 0, y: 50 }}  // Start with opacity 0 and 50px down from the original position
                        animate={isError ? { x: [0, 10, -10, 10, 0], opacity: 1, y: 0 } : { opacity: 1, y: 0 }}  // If there's an error, trigger a shake effect (x-axis movement); otherwise, fade in
                        transition={{ duration: 0.5, delay: 1.5 }}  // Animation delay and duration for smooth effect
                    >
                        {/* Sign-in form container */}
                        <Card color="transparent" shadow={false} className="p-10 rounded-lg w-[400px] h-[450px] bg-[#D9D9D9]">
                            {/* Title of the card */}
                            <Typography variant="h2" color="black" className="flex mb-4 text-center justify-center font-inter font-bold text-[32px]">
                                Sign In
                            </Typography>

                            {/* Form for email and password inputs */}
                            <form onSubmit={handleSubmit} className="mt-8 mb-2 w-full">
                                <div className="space-y-6 w-[300px]">
                                    
                                    {/* Email input field with hover and tap animations */}
                                    <motion.div
                                        className="relative"
                                        whileHover={{ scale: 1.05 }}  // Slight scale-up effect when hovering
                                        whileTap={{ scale: 0.95 }}  // Slight scale-down effect when clicking (tap)
                                    >
                                        <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-black">
                                            <MdOutlineMailOutline className="w-5 h-5" />
                                        </span>
                                        {/* Email input field styled with Material Tailwind */}
                                        <Input
                                            type="text"
                                            placeholder="Enter Your Email"
                                            className="pl-10 pr-3 py-2 w-full h-[41px] border rounded-md border-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-transparent text-black"
                                        />
                                    </motion.div>
                                    
                                    {/* Password input field with hover and tap animations */}
                                    <motion.div
                                        className="relative"
                                        whileHover={{ scale: 1.05 }}  // Similar hover effect for password field
                                        whileTap={{ scale: 0.95 }}  // Similar tap (click) effect
                                    >
                                        <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-black">
                                            <MdLockOutline className="w-5 h-5" />
                                        </span>
                                        {/* Password input field */}
                                        <Input
                                            type="password"
                                            placeholder="Enter Your Password"
                                            className="pl-10 pr-3 py-2 w-full h-[41px] border rounded-md border-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-transparent text-black"
                                        />
                                    </motion.div>

                                    {/* Forgot password link */}
                                    <div className="text-right mt-1">
                                        <Link to="/forgot-password" onClick={handleForgotPassword} className="text-sm text-blue-500 hover:underline">
                                            Forgot Password?
                                        </Link>
                                    </div>
                                    
                                    {/* Sign in button with hover and tap animations */}
                                    <div className="flex justify-center">
                                        <motion.button
                                            type="submit"
                                            className="bg-black hover:bg-red-700 text-white py-2 rounded-full w-[202px] h-[53px]"
                                            whileHover={{ scale: 1.05 }}  // Button slightly scales up when hovering
                                            whileTap={{ scale: 0.95 }}  // Button slightly scales down when clicked (tapped)
                                        >
                                            Sign In
                                        </motion.button>
                                    </div>
                                    
                                    {/* Link to sign up for new users */}
                                    <Typography className="text-center mt-4 text-black">
                                        {"Don't Have An Account?"} <Link to="/signup" className="text-blue-500 hover:underline">Sign Up</Link>
                                    </Typography>
                                </div>
                            </form>
                        </Card>
                    </motion.div>
                </div>
            </div>
        </div>
    );
};

Shake Animation: Provides a shaking effect when an error occurs during form submission.
Hover & Tap Animations: Input fields and the button respond to user actions, growing slightly on hover and shrinking on tap, making interactions feel responsive and dynamic.
Motion.div for Error: Uses Framer Motion to animate the form's appearance and position, improving the overall experience when elements are shown or hidden.

assets/landing.css

/* General container for the landing screen */
.landing-screen {
    width: 100%;
    height: 100vh;
    overflow: hidden;
    background: #000000;
}

/* Container for stars */
.stars {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
}

/* Style for each individual star */
.star {
    position: absolute;
    width: 2px;
    height: 2px;
    background: white;
}

/* Style for the small moon image */
.small-moon {
    position: absolute;
    top: .5px;
    left: .5px;
    width: 100px;
    height: auto;
}

/* Style for the logo image */
.logo {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 200px;
    height: auto;
}

/* Style for the large moon image */
.large-moon {
    position: absolute;
    bottom: 0;
    left: 70%;
    transform: translateX(-50%);
    height: auto;
}

/* Media queries for responsive design */
@media (max-width: 768px) {
    .small-moon {
        width: 70px;
    }
    .logo {
        width: 150px;
    }
    .large-moon {
        width: 120%;
    }
}

@media (max-width: 480px) {
    .small-moon {
        width: 50px;
    }
    .logo {
        width: 100px;
    }
    .large-moon {
        width: 150%;
    }
}

.landing-screen::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-image: 
        radial-gradient(white 1px, transparent 1px),
        radial-gradient(white 1px, transparent 1px);
    background-size: 
        calc(10vw + 10px) calc(10vh + 10px),
        calc(15vw + 15px) calc(15vh + 15px);
    background-position: 0 0, 10px 10px;
    animation: twinkle 10s infinite;
    opacity: 0.5;
}

@keyframes twinkle {
    0%, 100% { opacity: 0.5; }
    50% { opacity: 0.8; }
}

styles/input.css

@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';


@media (max-width: 500px) {
    .large-screen-only {
      display: none;
    }
  }
  
  @media (min-width: 501px) {
    .small-screen-only {
      display: none;
    }
  }

Add the @tailwind directives for each of Tailwind’s layers to your input CSS file

main.jsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import './styles/input.css'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Package.json

{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview",
     "watch:tailwind": "tailwindcss -i ./src/styles/input.css  -o ./src/styles/output.css --watch"	
  },
  "dependencies": {
    "@material-tailwind/react": "^2.1.10",
    "framer-motion": "^11.11.9",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-icons": "^5.3.0",
    "react-router-dom": "^6.27.0"
  },
  "devDependencies": {
    "@eslint/js": "^9.11.1",
    "@types/react": "^18.3.10",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.2",
    "autoprefixer": "^10.4.20",
    "eslint": "^9.11.1",
    "eslint-plugin-react": "^7.37.0",
    "eslint-plugin-react-hooks": "^5.1.0-rc.0",
    "eslint-plugin-react-refresh": "^0.4.12",
    "globals": "^15.9.0",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.14",
    "vite": "^5.4.8"
  }
}

Index.css

:root {
  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: light dark;
  color: rgba(237, 231, 231, 0.87);
  background-color: #0a0a0a;


  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}
a:hover {
  color: #535bf2;
}

body {
  margin: 0;
  display: flex;
  place-items: center;
  min-width: 320px;
  min-height: 100vh;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}
button:hover {
  border-color: #646cff;
}
button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
  :root {
    color: #213547;
    background-color: #ffffff;
  }
  a:hover {
    color: #747bff;
  }
  button {
    background-color: #f9f9f9;
  }
}

Lets run the tailwind build command

npm un watch:tailwind

Lets start our server

npm run dev

Here is our final output

Image

YEA BUDDY!!

In this guide, we successfully managed to create a very responsive form and also make the best of signup forms with the help of Framer motion

feel free to contribute to this repo

Here is the live app https://framer-get-started.vercel.app/ try it now

Happy Coding : )

Barack Ouma

About Barack Ouma

Software Engineer | AWS Enthusiast | Tech Writer
Building and scaling cloud infrastructures.
Expert in AWS architecture, cost optimization, and security.
Passionate about simplifying tech through writing and hands-on solutions.
Driven to solve real-world problems with innovative and Quality Software.