KodaSchool
Learn To Code
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??
topics
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:
- Understand the Basics of Framer Motion: Learn about key concepts such as the
motion
component, animation properties, and variants. - Implement Advanced Animation Techniques: Use animation controls and gesture animations to enhance user interactions.
- Optimize Performance and Accessibility: Learn best practices for performance optimization and accessibility considerations.
- Create Custom Animation Hooks: Develop reusable animation hooks to manage complex animation logic.
- Integrate Error Handling and Loading States: Use
Suspense
andErrorBoundary
to handle loading and error states gracefully.
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 stateanimate
: End statetransition
: How the animation plays outexit
: 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
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
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 : )
share
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.