Variants in 'motion/react'
In this blog, I have given an example on how to use variants in framer motion. I demonstrated the concept of variants and how to use them with an interactive example of an animated delete button. Using this you can create stunning web designs, microinteractions, landing page, great UI/UX
Variants are one of the strongest features in framer motion. When I encountered this first time it really took some time for me to understand it fully. But once you get going with this it's easy to express complex animations. In this blog I will demonstrate the concept of variants in step by step manner by creating an animated button. I assume that you have some intuition with layout animation. In case you still feel uneasy here is the previous blog discussing layout animation.
What we will build today
An interactive delete button that smoothly transitions between states - from "Delete" to "Are you sure?" on hover, then to "Deleting..." when clicked, and finally "Deleted successfully" with smooth animations throughout.
The Core Idea
The core idea behind variants is propagating animations from parent motion component to child motion component.
Installation
npm i motion/react
Setting Up: The Foundation
First things first, make your component a client component and import motion. Remember to import from 'motion/react' not 'framer-motion'.
'use client'
import React, { useState } from 'react'
import { IconChecks } from '@tabler/icons-react'
import { motion } from 'motion/react'
Now let's set up our state management:
function VariantDeleteButton() {
const [startDelete, setStartDelete] = useState(false)
const [isDeleted, setIsDeleted] = useState(false)
const [isHovered, setIsHovered] = useState(false)
return (
// Our code goes here
)
}
We're tracking three states:
isHovered: When user hovers over the buttonstartDelete: When the delete process beginsisDeleted: When deletion completes
Defining Variants:
Instead of writing animations inline, we define a variants object:
const slideVariants = {
rest: {
x: '0%',
justifyContent: 'center',
gap: '0rem',
paddingLeft: '0.5rem',
paddingRight: '0.5rem',
transition: {
type: "spring",
stiffness: 300,
damping: 20,
mass: 2,
}
},
hover: {
x: '-80%',
justifyContent: 'space-between',
gap: '0rem',
paddingLeft: '0.5rem',
paddingRight: '0.5rem',
paddingBottom: '0.1rem',
transition: {
type: "spring",
stiffness: 300,
damping: 20,
mass: 1,
}
}
}
What's happening here?
- We defined two states: rest and hover
- Each state has its own set of CSS properties and transitions
- When the button is in rest state, the inner content is centered (
x: '0%') - When it switches to hover state, the content slides left (
x: '-80%') revealing the delete icon
Spring transitions:
stiffness: 300- How fast the spring animates (higher = faster)damping: 20- How much bounce (lower = more bounce)mass: 2- The weight of the element (higher = slower, heavier feel) you can play with the value..
Here is a great resource where you can play with transitions values.
Building the Button Structure
Now let's build our button:
<motion.button
onHoverStart={() => setIsHovered(true)}
onHoverEnd={() => { if (!startDelete && !isDeleted) setIsHovered(false) }}
animate={isHovered || startDelete ? "hover" : "rest"}
onClick={handleAnimatedDeletebutton1}
className="relative overflow-hidden bg-red-700 w-[200px] py-1 px-8 rounded-full border-2 border-red-400"
>
{/* Content goes here */}
</motion.button>
Key points:
onHoverStartandonHoverEndcontrol the hover stateanimateprop switches between "rest" and "hover" states- Notice we're not passing animation objects - just state names from the
slideVariantswe defined above!
How the animation propagates from parent to child container
Here's where variants truly shine. Inside our button, we have a white sliding element which activates when user hovers over the button for showing a confirmation message:
<motion.span
variants={slideVariants}
className="absolute inset-0 flex items-center bg-white rounded-full text-red-700"
>
{isDeleted ? (
<motion.span> Deleted successfully </motion.span>
) : (
<motion.span> Delete </motion.span>
)}
<motion.div>
{isDeleted ? (
<IconChecks size={20} />
) : (
<AnimatedTrashIcon rotation={startDelete ? 20 : 0} />
)}
</motion.div>
</motion.span>
See what happened?
- In this child
motion.spanhasvariants={slideVariants}prop - The parent button has
animate="hover"oranimate="rest"depending on the state - The child automatically inherits and applies the matching variant state!
- No need to duplicate animation logic - variants propagate from parent to children
This is the core concept of variants. The parent controls which state is active, and children with matching variant names automatically animate.
Adding the Delete Logic
Let's handle the click interaction:
const handleAnimatedDeletebutton1 = () => {
setStartDelete(true)
setTimeout(() => {
setIsDeleted(true)
setStartDelete(false)
}, 5000)
setTimeout(() => {
setIsHovered(false)
}, 7000)
}
The flow:
- User clicks →
startDeletebecomes true - After 5 seconds → Show "Deleted successfully"
- After 7 seconds → Reset hover state Here you need to connect your backend, for now I just added some time for demonstration.
Another small detail
There is subtle animation in the texts which makes this interaction more lively. For the text animation, we create individual letter animations:
{["i", "n", "g"].map((letter, index) => (
<motion.span
key={`ing-${index}`}
className='inline-block'
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: "spring",
damping: 20,
stiffness: 350,
delay: index * 0.1
}}
>
{letter}
</motion.span>
))}
Each letter animates in with a staggered delay (delay: index * 0.1), creating a typewriter effect.
The Complete Code
Here's everything put together:
'use client'
import React, { useState } from 'react'
import { IconChecks } from '@tabler/icons-react'
import { motion } from 'motion/react'
function VariantDeleteButton() {
const [startDelete, setStartDelete] = useState(false)
const [isDeleted, setIsDeleted] = useState(false)
const [isHovered, setIsHovered] = useState(false)
const handleAnimatedDeletebutton1 = () => {
setStartDelete(true)
setTimeout(() => {
setIsDeleted(true)
setStartDelete(false)
}, 5000)
setTimeout(() => {
setIsHovered(false)
}, 7000)
}
const slideVariants = {
rest: {
x: '0%',
justifyContent: 'center',
gap: '0rem',
paddingLeft: '0.5rem',
paddingRight: '0.5rem',
transition: {
type: "spring",
stiffness: 300,
damping: 20,
mass: 2,
}
},
hover: {
x: '-80%',
justifyContent: 'space-between',
gap: '0rem',
paddingLeft: '0.5rem',
paddingRight: '0.5rem',
paddingBottom: '0.1rem',
transition: {
type: "spring",
stiffness: 300,
damping: 20,
mass: 1,
}
}
}
return (
<div>
<motion.button
onHoverStart={() => setIsHovered(true)}
onHoverEnd={() => { if (!startDelete && !isDeleted) setIsHovered(false) }}
animate={isHovered || startDelete ? "hover" : "rest"}
onClick={handleAnimatedDeletebutton1}
className="relative overflow-hidden bg-red-700 w-[200px] py-1 px-8 rounded-full border-2 border-red-400"
>
<motion.span className="pl-4 flex items-center justify-center gap-2 text-white">
<motion.span className='flex'>
{startDelete && !isDeleted ?
(
<>
{"Delet"}
{["i", "n", "g"].map((letter, index) => (
<motion.span
key={`ing-${index}`}
className='inline-block'
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: "spring",
damping: 20,
stiffness: 350,
delay: index * 0.1
}}
>
{letter}
</motion.span>
))}
</>
) : !isDeleted ? (
"Are you sure?"
) : (
<>
{"Delet"}
{["e", "d"].map((letter, index) => (
<motion.span
key={`ed-${index}`}
className='inline-block'
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: "spring",
damping: 20,
stiffness: 350,
delay: index * 0.1
}}
>
{letter}
</motion.span>
))}
</>
)
}
</motion.span>
</motion.span>
<motion.span
variants={slideVariants}
className="absolute inset-0 flex items-center bg-white rounded-full text-red-700 pointer-events-none"
>
{isDeleted ? (
<motion.span> Deleted successfully </motion.span>
) : (
<motion.span> Delete </motion.span>
)}
<motion.div>
{isDeleted ? (
<IconChecks size={20} />
) : (
<AnimatedTrashIcon rotation={startDelete ? 20 : 0} />
)}
</motion.div>
</motion.span>
</motion.button>
</div>
)
}
export default VariantDeleteButton
const AnimatedTrashIcon = ({ rotation }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="-2 -6 28 32"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="icon icon-tabler icons-tabler-outline icon-tabler-trash"
>
<g
style={{
overflow: 'visible',
transformOrigin: 'right bottom',
transformBox: 'fill-box',
transform: `rotate(${rotation}deg)`,
transition: 'transform 0.3s ease-out'
}}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
<path d="M4 7l16 0" />
</g>
<path d="M10 11l0 6" />
<path d="M14 11l0 6" />
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
</svg>
)
Key Takeaways
To cut the long story short:
- Variants are animation blueprints - Define once, use everywhere
- Parent controls, children follow - Set
animateon parent, variants propagate to children - Keep it organized - Complex animations become manageable with named states
- Spring transitions - Play with stiffness, damping, and mass for natural motion
- State management matters - Use React state to control which variant is active
Why Variants Are Powerful
Without variants, you'd write this:
<motion.span
animate={{
x: isHovered ? '-80%' : '0%',
justifyContent: isHovered ? 'space-between' : 'center',
// ... more properties
}}
transition={{
type: "spring",
stiffness: 300,
// ... more config
}}
>
With variants, you write:
<motion.span variants={slideVariants}>
Much cleaner! And if you have multiple children, they all inherit the state automatically.
Check out my projects, my designs I have built in recent times.
Hope you learnt something new. Share with your friends. Tag me on twitter.
I will be back with new concepts. See you next time!
