Installation https://www.framer.com/motion/introduction/#quick-start npm i framer-motion - install import { motion } from "framer-motion" - import Some info https://egghead.io/lessons/react-create-micro-interactions-with-framer-motion-gesture-props https://www.youtube.com/playlist?list=PLNG2YBDrzK-yhlQtfsrzzQvaLDVj-pMXI https://www.youtube.com/playlist?list=PL4cUxeGkcC9iHDnQfTHEVVceOEBsOf07i Simple animation
import { motion } from 'framer-motion'
const Example1 = () => (
<motion.div
initial={{ opacity: 0, fontSize: '0px', color: '#fff', x: '-100vw' }}
animate={{ opacity: 1, fontSize: '30px', color: '#ff2994', x: 0 }}
transition={{ delay: 0.5, duration: 1.5, type: 'spring' }}
>
Animated text
</motion.div>
)
Hover
<motion.button
whileHover={{ scale: 1.2 }}
>
Animated hover
</motion.button>
With MUI
<Button
variant="contained"
component={motion.div}
whileHover={{
scale: 1.2,
transition: { duration: 0.3 }
}}
>
Animated hover
</Button>
Variants Variants allows to extract initial , animate & transition props into an external object and reference it in multiple components. It makes code cleaner
const containerVariants = {
hidden: { opacity: 0, x: '-100vw' },
visible: { opacity: 1, x: 0, transition: { type: 'spring', delay: 1} }
}
const Example4 = () => (
<motion.div
variants={containerVariants}
initial='hidden'
animate='visible'
css={{ fontSize: '30px' }}
>
Animated text
</motion.div>
)
Variants props propagation If we have a parent motions element with enabled variants And it has children with another motions elements with same variant prop Then children will inherit initial and animate props from the parent, unless not specified other values
const containerVariants = {
hidden: { opacity: 0, x: '-100vw' },
visible: { opacity: 1, x: 0, transition: { type: 'spring', delay: 1 } }
}
const Example5 = ({ children }) => (
<motion.div
variants={containerVariants}
initial='hidden'
animate='visible'
css={{ fontSize: '30px' }}
>
Parent motion
{ children }
</motion.div>
)
const Example6 = ({ children }) => (
<motion.div
variants={containerVariants}
css={{ color: 'grey' }}
>
Child motion
</motion.div>
)
<Example5>
<Example6 />
</Example5>
beforeChildren We have animated parent and children By default animations starts at the same time everywhere Usually we want parent to animate and only after start children animation We may play with delays, but that is not elegant With variants we can use when prop of transition with beforeChildren value to achieve it
const parentVariants = {
hidden: { x: '-100vw' },
visible: { x: 0, transition: { type: 'spring', delay: 1, when: 'beforeChildren' } }
}
const childVariants = {
hidden: { y: '100%' },
visible: { y: 0 }
}
const Example7 = ({ children }) => (
<motion.div
variants={parentVariants}
initial='hidden'
animate='visible'
css={{ fontSize: '30px', overflow: 'hidden' }}
>
Parent motion
<motion.div
variants={childVariants}
css={{ color: 'grey', fontSize: '16px' }}
>
Child motion
</motion.div>
</motion.div>
)
Stagger children
const parentVariants2 = {
hidden: { x: '-100vw' },
visible: {
x: 0,
transition: {
type: 'spring',
delay: 1,
when: 'beforeChildren',
staggerChildren: 0.5
}
}
}
const childVariants2 = {
hidden: { y: 100 },
visible: { y: 0 }
}
const Example8 = ({ children }) => (
<motion.div
variants={parentVariants2}
initial='hidden'
animate='visible'
css={{ fontSize: '30px', overflow: 'hidden' }}
>
Parent motion
<motion.div
variants={childVariants2}
css={{ color: 'grey', fontSize: '16px' }}
>
Child motion 1
</motion.div>
<motion.div
variants={childVariants2}
css={{ color: 'grey', fontSize: '16px' }}
>
Child motion 2
</motion.div>
<motion.div
variants={childVariants2}
css={{ color: 'grey', fontSize: '16px' }}
>
Child motion 3
</motion.div>
</motion.div>
)
Keyframe
const Example9 = () => (
<motion.button
whileHover={{ scale: [1.2, 1, 1.2, 1, 1.2, 1, 1.2, 1, 1.2, 1, 1.2] }}
>
Animated hover with keyframes
</motion.button>
)
Repeating animation
const Example10 = () => (
<motion.button
whileHover={{ scale: 1.2 }}
transition={{ repeat: Infinity }}
>
Animated hover with yo-yo
</motion.button>
)
Animate unmount Before we just animated a component's appetence With AnimatePresence can animate also removal of a component
import { motion, AnimatePresence } from 'framer-motion'
const Example11 = () => {
const [isShow, setIsShow] = useState(false)
return (
<>
<AnimatePresence>
{isShow && (
<motion.h2
initial={{ x: '-100vw' }}
animate={{ x: 0 }}
exit={{ x: '100vw' }}
>
Animated text
</motion.h2>
)}
</AnimatePresence>
<button onClick={() => setIsShow(!isShow)}>Toggle text</button>
</>
)
}
onAnimationComplete That is how I made a slide effect between invoices at my work.
import { motion, AnimatePresence } from 'framer-motion'
export const Invoice = () => {
const dispatch = useDispatch()
const { data: user } = useUserQuery()
const closeDialog = () => dispatch(handleCloseInvoice(user))
const { id } = useParams()
// disable scroll on body due to custom backdrop
//! extract into a custom hook
useEffectOnce(() => {
document.body.style.overflow = 'hidden'
return () => {
document.body.style.overflow = 'auto'
}
})
useUnmount(() => {
queryClient.invalidateQueries({ queryKey: ['invoices'] })
setTimeout(() => { queryClient.invalidateQueries({ queryKey: ['invoices'] }) }, 5000)
})
return (
<Backdrop
open={!!id}
sx={{
background: '#00000080',
zIndex: 666
}}
>
<AnimatePresence
initial={false}
mode='wait'
>
<motion.div
key={id}
initial={{ x: '100vw' }}
animate={{ x: 0 }}
exit={{ x: '-100vw' }}
transition={{ ease: 'linear' }}
onAnimationComplete={(definition) => {
// triggered twice on exit and enter animation completion
if (definition.x === 0) dispatch(handleInvoiceOpened(id, user))
}}
>
<Box
data-testid='invoice-container'
sx={{
display: 'flex',
flexDirection: 'column',
width: '95vw',
height: '95vh',
maxWidth: '95vw',
maxHeight: '95vh',
zIndex: 1000
}}
>
<InvoiceTitle closeDialog={closeDialog} />
<InvoiceContent />
<InvoiceFooter closeDialog={closeDialog} />
</Box>
</motion.div>
</AnimatePresence>
</Backdrop>
)
}
Animate svg
const Example12 = () => {
return (
<svg
viewBox="0 0 100 20"
xmlns="http://www.w3.org/2000/svg"
css={{
fill: 'none',
stroke: 'black',
strokeWidth: 2,
strokeDasharray: 10
}}
>
<motion.path
d="M 0,10 h100"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 2, ease: 'easeInOut', delay: 2 }}
/>
</svg>
)
}
Loader
const Example13 = () => {
return (
<motion.div
css={{
width: '10px',
height: '10px',
margin: '40px auto',
borderRadius: '50%',
background: 'grey'
}}
animate={{
x: [-20, 20],
y: [0, -30]
}}
transition={{
x: {
duration: 0.6,
repeat: Infinity,
repeatType: 'mirror'
},
y: {
duration: 0.3,
repeat: Infinity,
repeatType: 'mirror',
ease: 'easeInOut'
}
}}
>
</motion.div>
)
}
Change between animation properties
const Example14 = () => {
const [x, cycleX] = useCycle(0, 50, 100)
return (
<>
<motion.div
css={{
width: '10px',
height: '10px',
background: 'red',
borderRadius: '50%'
}}
animate={{ x }}
/>
<button onClick={() => cycleX()}>Change position</button>
</>
)
}
Change between animation variants
const variants = {
animationOne: {
x: [-20, 20],
y: 0,
transition: {
x: {
duration: 0.6,
repeat: Infinity,
repeatType: 'mirror'
}
}
},
animationTwo: {
x: 0,
y: [-20, 20],
transition: {
y: {
duration: 0.6,
repeat: Infinity,
repeatType: 'mirror'
}
}
}
}
const Example15 = () => {
const [animation, cycleAnimation] = useCycle('animationOne', 'animationTwo')
return (
<>
<motion.div
css={{
width: '10px',
height: '10px',
margin: '40px auto',
borderRadius: '50%',
background: 'green'
}}
variants={variants}
animate={animation}
>
</motion.div>
<button onClick={() => cycleAnimation()}>Change animation variant</button>
</>
)
}
Layout & Custom prop with custom prop we may send data to variants for dynamic animation with layoutId prop we may animate movements of siblings when this element is added or removed
const itemsStackVariants = {
hidden: {
y: -100,
opacity: 0,
transition: {
duration: 0.3
}
},
visible: (num) => ({
y: 0,
opacity: 1,
transition: {
duration: 0.3,
delay: 0.05 * num
}
})
}
function Example16() {
const [items, setItems] = useState([5, 4, 3, 2, 1])
return (
<>
<button
onClick={() => {
setItems([items[0] + 1, ...items])
}}
>
add
</button>
<button
onClick={() => {
if (items.length < 2) return
setItems(items.slice(1))
}}
>
remove
</button>
<div>{JSON.stringify(items)}</div>
<div style={{ marginTop: 50 }}>
<AnimatePresence initial={true}>
{items.map((num) => (
<motion.div
key={num}
variants={itemsStackVariants}
initial="hidden"
animate="visible"
exit="hidden"
layoutId={num}
custom={num}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
style={{
padding: '10px 20px',
marginBottom: 2,
border: '1px solid firebrick',
whiteSpace: 'nowrap',
cursor: 'pointer'
}}
>
item {num}
</motion.div>
))}
</AnimatePresence>
</div>
</>
)
}