How to use Variants with Framer Motion useAnimation - reactjs

I need my animation to show when it reaches viewport with this logic below from stack overflow:
import { useInView } from "react-intersection-observer";
import { motion, useAnimation } from "framer-motion";
const Component = () => {
const animation = useAnimation();
const [ref, inView, entry] = useInView({ threshold: 0.1 });
useEffect(() => {
if (inView) {
animation.start("visible");
} else {
animation.start("hidden");
}
}, [animation, inView]);
const variants = {
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.5, delayChilden: 0.2, staggerChildren: 0.1 },
},
hidden: {
y: enter,
opacity: 0,
},
}
return (
<>
<motion.div
ref={ref}
animate={animation}
initial="hidden"
variants={{variants}}>TITLE 1</motion.div>
//---------CONTENTS HERE
<motion.div
ref={ref}
animate={animation}
initial="hidden"
variants={{variants}}>TITLE 2</motion.div>
//---------CONTENTS HERE
<motion.div
ref={ref}
animate={animation}
initial="hidden"
variants={{variants}}>TITLE 3</motion.div>
</>
);
}
But using variants to reuse animation for different headings, the animation loads no longer until I reach the last heading of the page
I thought of having different variants for each title but that would just make my code messy.
How do I sort this out?

So I figured out the way to do this, all you have to do is have as many useAnimation and inView needed for the number of elements you are going to apply
In my case, that's 3 useAnimation & inView for 3 titles
const title1 = useAnimation()
const title2 = useAnimation()
const title3 = useAnimation()
const [ref, inView, entry] = useInView({ threshold: 0.5 });
const [ref1, inView2] = useInView({ threshold: 0.5 });
useEffect(() => {
if (inView) {
animation.start("visible")
} else {
animation.start("hidden");
}
if (inView2) {
animation2.start("visible")
} else {
animation2.start("hidden");
}
}, [inView, inView2]);
Voila

Related

How to do a 3D carousel wth React

I cannot find how to do a 3D carousel (aka slideshow) with React being able to show at least three elements.
There seems not to be up-to-date libraries or components for that in npm :(
Here is what it should look like:
After experimenting for a while, here is how I manage to do it using framer motion:
import './styles.css';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
export default function App() {
const [[activeIndex, direction], setActiveIndex] = useState([0, 0]);
const items = ['🍔', '🍕', '🌭', '🍗'];
// we want the scope to be always to be in the scope of the array so that the carousel is endless
const indexInArrayScope =
((activeIndex % items.length) + items.length) % items.length;
// so that the carousel is endless, we need to repeat the items twice
// then, we slice the the array so that we only have 3 items visible at the same time
const visibleItems = [...items, ...items].slice(
indexInArrayScope,
indexInArrayScope + 3
);
const handleClick = newDirection => {
setActiveIndex(prevIndex => [prevIndex[0] + newDirection, newDirection]);
};
return (
<div className="main-wrapper">
<div className="wrapper">
{/*AnimatePresence is necessary to show the items after they are deleted because only max. 3 are shown*/}
<AnimatePresence mode="popLayout" initial={false}>
{visibleItems.map((item) => {
// The layout prop makes the elements change its position as soon as a new one is added
// The key tells framer-motion that the elements changed its position
return (
<motion.div
className="card"
key={item}
layout
custom={{
direction,
position: () => {
if (item === visibleItems[0]) {
return 'left';
} else if (item === visibleItems[1]) {
return 'center';
} else {
return 'right';
}
},
}}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 1 }}
>
{item}
</motion.div>
);
})}
</AnimatePresence>
</div>
<div className="buttons">
<motion.button
whileTap={{ scale: 0.8 }}
onClick={() => handleClick(-1)}
>
◀︎
</motion.button>
<motion.button whileTap={{ scale: 0.8 }} onClick={() => handleClick(1)}>
▶︎
</motion.button>
</div>
</div>
);
}
const variants = {
enter: ({ direction }) => {
return { scale: 0.2, x: direction < 1 ? 50 : -50, opacity: 0 };
},
center: ({ position }) => {
return {
scale: position() === 'center' ? 1 : 0.7,
x: 0,
zIndex: zIndex[position()],
opacity: 1,
};
},
exit: ({ direction }) => {
return { scale: 0.2, x: direction < 1 ? -50 : 50, opacity: 0 };
},
};
const zIndex = {
left: 1,
center: 2,
right: 1,
};
Here is a code sandbox with the solution:
https://codesandbox.io/s/react-3d-carousel-wth-framer-motion-rtn6vx?file=/src/App.js

Dynamically declare framer motion html element type

I am trying to create a reusable component that will use a consistent transition throughout the app. In doing so, i've create a a div that uses framer-motion to animate. I'd like to be able to tell the component that its a div, span, etc.. via a prop.
Calling it like so:
<AnimatedEl el={'div'}>
...
</AnimatedEl>
import { AnimatePresence, motion } from 'framer-motion'
interface AnimatedDivProps {
el: string
children: React.ReactNode
className?: string
}
const AnimatedDiv = ({ el, className, children }: AnimatedDivProps) => {
const transition = {
duration: 0.8,
ease: [0.43, 0.13, 0.23, 0.96],
}
return (
<AnimatePresence>
<motion[el]
className={className}
initial='exit'
animate='enter'
exit='exit'
variants={{
exit: { y: 100, opacity: 0, transition },
enter: { y: 0, opacity: 1, transition: { delay: 0.2, ...transition } },
}}
>
{children}
</motion[el]>
</AnimatePresence>
)
}
export default AnimatedDiv
You can create dynamic framer motion elements by creating a new component that is assigned to the motion function, with the element type passed in. Then use that new component in place of motion[el].
so inside AnimatedDiv add:
const DynamicMotionComponent = motion(el);
then in the return statement use it like this:
<DynamicMotionComponent
className={className}
initial='exit'
animate='enter'
exit='exit'
variants={{
exit: { y: 100, opacity: 0, transition },
enter: { y: 0, opacity: 1, transition: { delay: 0.2, ...transition } },
}}
>
{children}
</DynamicMotionComponent>
If you log the typeof motion and the typeof motion.div, you can see motion isn't an object but a callable function.
console.log(typeof motion) // => function
where as
console.log(typeof motion.div) // => object
Here's a different example of a wrapper component with a similar concept that creates motion elements based off of the child components. It fades in, animates y values and staggers children when the element is in view, the children could be a set of divs, svg's etc... it modifies the className prop of the child react component and applies the Framer Motion variants prop, here's how I achieved it:
import {
Children,
cloneElement,
isValidElement,
ReactChild,
ReactFragment,
ReactNode,
ReactPortal,
useEffect,
useRef,
} from "react";
import {
motion,
useAnimationControls,
useInView,
Variants,
} from "framer-motion";
import CONSTANTS from "#/lib/constants";
import styles from "#/styles/components/motionFadeAndStaggerChildrenWhenInView/motionFadeAndStaggerChildrenWhenInView.module.scss";
interface MotionFadeAndStaggerChildrenWhenInView {
childClassName?: string;
children: ReactNode;
className?: string;
variants?: Variants;
}
type Child = ReactChild | ReactFragment | ReactPortal;
const parentVariants = {
fadeInAndStagger: {
transition: {
delayChildren: 0.3,
ease: CONSTANTS.swing,
staggerChildren: 0.2,
},
},
initial: {
transition: {
ease: CONSTANTS.swing,
staggerChildren: 0.05,
staggerDirection: -1,
},
},
};
const childVariants = {
fadeInAndStagger: {
opacity: 1,
transition: {
ease: CONSTANTS.swing,
y: { stiffness: 1000, velocity: -100 },
},
y: 0,
},
initial: {
opacity: 0,
transition: {
ease: CONSTANTS.swing,
y: { stiffness: 1000 },
},
y: 50,
},
};
// eslint-disable-next-line #typescript-eslint/no-redeclare -- intentionally naming the variable the same as the type
const MotionFadeAndStaggerChildrenWhenInView = ({
childClassName,
children,
className,
variants = childVariants,
}: MotionFadeAndStaggerChildrenWhenInView) => {
const childrenArray = Children.toArray(children);
const childClassNames =
childClassName !== undefined
? `${childClassName} ${styles.fadeAndStaggerChild}`
: styles.fadeAndStaggerChild;
const controls = useAnimationControls();
const ref = useRef<HTMLDivElement | null>(null);
const isInView = useInView(ref, { once: true });
useEffect(() => {
if (isInView) {
// eslint-disable-next-line #typescript-eslint/no-floating-promises
controls.start("fadeInAndStagger");
}
}, [controls, isInView]);
return (
<motion.div
ref={ref}
animate={controls}
className={
className
? `${styles.fadeAndStaggerParent} ${className}`
: styles.fadeAndStaggerParent
}
initial="initial"
variants={parentVariants}
>
{Children.map(childrenArray, (child: Child) => {
if (!isValidElement(child)) return null;
if (isValidElement(child)) {
const propsClassNames: string =
// eslint-disable-next-line #typescript-eslint/no-unsafe-argument
Object.hasOwn(child.props, "className") === true
? // eslint-disable-next-line #typescript-eslint/no-unsafe-member-access
(child.props.className as string)
: "";
const DynamicMotionComponent = motion(child.type);
// eslint-disable-next-line #typescript-eslint/no-unsafe-argument
return cloneElement(<DynamicMotionComponent />, {
...child.props,
className: propsClassNames
? `${childClassNames} ${propsClassNames}`
: childClassNames,
variants,
});
}
return null;
})}
</motion.div>
);
};
export default MotionFadeAndStaggerChildrenWhenInView;

Framer animation using react intersection observer. Need multiple animations but get only one

I'm trying to assign the same animation to multiple instances of a component, using Framer Motion and the react-intersection-observer package
import { useEffect, useRef, useCallback } from "react";
import { motion, useAnimation } from "framer-motion";
import { useInView } from "react-intersection-observer";
const levels = [
{
title: "GROUP LESSONS",
description:
"Lorem ipsum",
},
{
title: "WORKSHOPS",
description:
"Lorem ipsum",
},
];
const container = {
show: {
transition: {
staggerChildren: 0.2,
},
},
};
const item = {
hidden: { opacity: 0, x: 200 },
show: {
opacity: 1,
x: 0,
transition: {
ease: [0.6, 0.01, -0.05, 0.95],
duration: 1.6,
},
},
};
const Levels = () => {
const animation = useAnimation();
const [levelRef, inView] = useInView({
triggerOnce: true,
});
useEffect(() => {
if (inView) {
animation.start("show");
}
}, [animation, inView]);
return (
<LevelsContainer>
{levels.map((level, index) => {
return (
<LevelsWrapper
key={index}
ref={levelRef}
animate={animation}
initial="hidden"
variants={container}
>
<Level variants={item}>
<Title>{level.title}</Title>
<Description>{level.description}</Description>
</Level>
</LevelsWrapper>
);
})}
</LevelsContainer>
);
};
This results in the animation loading only when scrolling to the last LevelWrapper component. Then "inView" is set to true and all the components animate at the same time. In the react-intersection-observer package documentation, there's some info about wrapping multiple ref assignments in a single useCallback, so I've tried that:
const animation = useAnimation();
const ref = useRef();
const [levelRef, inView] = useInView({
triggerOnce: true,
});
const setRefs = useCallback(
(node) => {
ref.current = node;
levelRef(node);
},
[levelRef]
);
useEffect(() => {
if (inView) {
animation.start("show");
}
}, [animation, inView]);
return (
<LevelsContainer>
{levels.map((level, index) => {
return (
<LevelsWrapper
key={index}
ref={setRefs}
animate={animation}
initial="hidden"
variants={container}
>
<Level variants={item}>
<Title>{level.title}</Title>
<Description>{level.description}</Description>
</Level>
</LevelsWrapper>
);
})}
</LevelsContainer>
);
But the animations still don't trigger individually for each LevelWrapper component. What's happening?
No idea why the code in the question doesn't work but I found the final result can be reached without using neither useEffect, useRef, useCallback, , useAnimation or useInView.
In the Framer Motion documentation:
Motion extends the basic set of event listeners provided by React with a simple yet powerful set of UI gesture recognisers.
It currently has support for hover, tap, pan, viewport and drag
gesture detection. Each gesture has a series of event listeners that
you can attach to your motion component.
Then applied whats explained here: https://www.framer.com/docs/gestures/#viewport-options
<LevelsWrapper
key={index}
initial="hidden"
whileInView="show"
variants={container}
viewport={{ once: true, amount: 0.8, margin: "200px" }}
>

Why are my dimensions not updating on resize when using React Hooks

My dimensions are not updating whenever the window is resized. In the code below you can see the window.innerHeight is updated, but the dimensions are not. I am probably missing something but I have not figured it out yet.
// Navbar.components.ts:
export const sidebar = () => {
let height;
let width;
if (typeof window !== `undefined`) {
height = window.innerHeight
width = window.innerWidth
}
const [dimensions, setDimensions] = useState({
windowHeight: height,
windowWidth: width,
})
useEffect(() => {
const debouncedHandleResize = debounce(function handleResize() {
setDimensions({
windowHeight: window.innerHeight,
windowWidth: window.innerWidth,
});
// Logging window.innerHeight gives the current height,
// Logging dimensions.windowHeight gives the initial height
console.log(window.innerHeight, " . ", dimensions.windowHeight)
}, 100);
window.addEventListener(`resize`, debouncedHandleResize)
return () => window.removeEventListener('resize', debouncedHandleResize)
}, [])
return {
open: () => ({
clipPath: `circle(${dimensions.windowHeight * 2 + 200}px at 40px 40px)`,
transition: {
type: "spring",
stiffness: 20,
restDelta: 2
}
}),
closed: () => ({
clipPath: `circle(30px at ${300 - 40}px ${dimensions.windowHeight - 45}px)`,
transition: {
delay: 0.2,
type: "spring",
stiffness: 400,
damping: 40
}
})
}
}
And I use the sidebar like this:
// Navbar.tsx
const Navbar: React.FC<NavbarProps> = () => {
...
return {
...
<MobileNavBackground variants={sidebar()} />
...
}
}
Here is an example of the logs that are returned when resizing the window:
Update 1
#sygmus1897
Code changed to this:
// Navbar.tsx:
const Navbar: React.FC<NavbarProps> = () => {
const [windowWidth, windowHeight] = getDimensions();
useEffect(() => {
}, [windowWidth, windowHeight])
return (
...
<MobileNavWrapper
initial={false}
animate={menuIsOpen ? "open" : "closed"}
custom={height}
ref={ref}
menuIsOpen={menuIsOpen}
>
<MobileNavBackground variants={sidebar} custom={windowHeight} />
<MobileNav menuIsOpen={menuIsOpen} toggleMenu={toggleMenu} />
<MenuToggle toggle={() => toggleMenu()} />
</MobileNavWrapper>
)
}
// getDimensions()
export const getDimensions = () => {
const [dimension, setDimension] = useState([window.innerWidth, window.innerHeight]);
useEffect(() => {
window.addEventListener("resize", () => {
setDimension([window.innerWidth, window.innerHeight])
});
return () => {
window.removeEventListener("resize", () => {
setDimension([window.innerWidth, window.innerHeight])
})
}
}, []);
return dimension;
};
// Navbar.components.ts
export const sidebar = {
open: (height) => ({
clipPath: `circle(${height + 200}px at 40px 40px)`,
transition: {
type: "spring",
stiffness: 20,
restDelta: 2
}
}),
closed: (height) => ({
clipPath: `circle(30px at ${300 - 60}px ${height - 65}px)`,
transition: {
delay: 0.2,
type: "spring",
stiffness: 400,
damping: 40
}
})
}
The issue remains where resizing the window does not affect the clipPath position of the circle. To illustrate the issue visually, the hamburger is supposed to be inside the green circle:
You can make a custom hook to listen to window resize.
You can modify solution from this link as per you requirement Custom hook for window resize
By using useState instead of ref, updating it on resize and returning the values to your main component
Here's an example:
export default function useWindowResize() {
const [dimension, setDimension] = useState([0, 0]);
useEffect(() => {
window.addEventListener("resize", () => {
setDimension([window.innerWidth, window.innerHeight])
});
return () => {
window.removeEventListener("resize", () => {
setDimension([window.innerWidth, window.innerHeight])
})
}
}, []);
return dimension;
}
and inside your main component use it like this:
const MainComponent = () => {
const [width, height] = useWindowResize();
useEffect(()=>{
// your operations
}, [width, height])
}
Your component will update every time the dimensions are changed. And you will get the updated width and height
EDIT:
Framer-motion provides a way to dynamically set variant's properties(for detailed guide refer to this Dynamically Update Variant) :-
// Navbar.tsx:
const Navbar: React.FC<NavbarProps> = () => {
return (
...
<MobileNavWrapper
initial={false}
custom={window.innerWidth} // custom={window.innerHeight} if variable depends on Height
animate={menuIsOpen ? "open" : "closed"}
custom={height}
ref={ref}
menuIsOpen={menuIsOpen}
>
<MobileNavBackground variants={sidebar} />
<MobileNav menuIsOpen={menuIsOpen} toggleMenu={toggleMenu} />
<MenuToggle toggle={() => toggleMenu()} />
</MobileNavWrapper>
)
}
// Navbar.components.ts
export const sidebar = {
open: (width) => ({
clipPath: `circle(${width+ 200}px at 40px 40px)`,
transition: {
type: "spring",
stiffness: 20,
restDelta: 2
}
}),
closed: (width) => ({
clipPath: `circle(30px at ${300 - 60}px ${width- 65}px)`,
transition: {
delay: 0.2,
type: "spring",
stiffness: 400,
damping: 40
}
})
}
Thanks to this thread: Framer Motion - stale custom value - changing the custom value doesn't trigger an update I found that the issue I'm having is a bug in framer-motion. To resolve this issue, add a key value to the motion component that's having issues re-rendering. This makes sure React re-renders the component.
In my case all I had to do was this:
<MobileNavBackground variants={sidebar} custom={windowHeight} key={key} />

Framer: Check if element is into viewport

While using Framer Motion API to create interaction and animations on my site, I can not find how to use it in order to trigger an animation when something is on the screen.
For example, this SVG draws correctly, but Framer does not wait for the element to be on the viewport and triggers it right after loading site:
import React, { Component } from 'react'
import { motion } from "framer-motion";
class IsometricScreen extends Component {
constructor() {
super()
this.icon = {
hidden: { pathLength: 0 },
visible: { pathLength: 1 }
}
}
render() {
return (
<motion.svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 1000" className="svg-mobile">
<motion.path
d="M418,988.93H82c-39.76,0-72-32.24-72-72V83.07c0-39.76,32.24-72,72-72h336c39.76,0,72,32.24,72,72v833.86
C490,956.69,457.76,988.93,418,988.93z"
variants={this.icon}
initial="hidden"
animate="visible"
transition={{
default: { duration: 2, ease: "easeInOut" }
}}
/>
</motion.svg>
)
}
}
export default IsometricScreen
Does Framer have a viewport detection triggerer to be implemented here?
Alternatively, you can use Intersection Observer, blends pretty well with React and framer motion.
import { useInView } from "react-intersection-observer"; // 1.9K gzipped
import { motion, useAnimation } from "framer-motion";
const Component = () => {
const animation = useAnimation();
const [ref, inView, entry] = useInView({ threshold: 0.1 });
useEffect(() => {
if (inView) {
animation.start("visible");
} else {
animation.start("hidden");
}
}, [animation, inView]);
const variants = {
visible: {
y: 0,
opacity: 1,
transition: { duration: 0.5, delayChilden: 0.2, staggerChildren: 0.1 },
},
hidden: {
y: enter,
opacity: 0,
},
}
return (
<motion.div
ref={ref}
animate={animation}
initial="hidden"
variants={{variants}}
/>
);
}
You can also refine your animation by looking at entry object (entering from top or bottom, etc)
framer-motion has built-in support for this use case since version 5.3.
Here's a CodeSandbox demonstrating the pattern: https://codesandbox.io/s/framer-motion-animate-in-view-5-3-94j13
Relevant code:
function FadeInWhenVisible({ children }) {
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ duration: 0.3 }}
variants={{
visible: { opacity: 1, scale: 1 },
hidden: { opacity: 0, scale: 0 }
}}
>
{children}
</motion.div>
);
}
Usage:
<FadeInWhenVisible>
<Box />
</FadeInWhenVisible>
I have finally solved this with a tiny functional component:
function inViewport() {
const isInViewport = el => {
const rect = el.getBoundingClientRect()
const vertInView = (rect.top <= window.innerHeight) && ((rect.top + rect.height) >= 0)
const horInView = (rect.left <= window.innerWidth) && ((rect.left + rect.width) >= 0)
return (vertInView && horInView)
}
this.elms = document.querySelectorAll('.showOnScreen')
window.addEventListener("scroll", () => {
this.elms.forEach(elm => isInViewport(elm) ? elm.classList.add('visible') : elm.classList.remove('visible'))
})
}
export default inViewport

Resources