Framer: Check if element is into viewport - reactjs

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

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;

How to use Variants with Framer Motion useAnimation

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

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" }}
>

I want to slow down the animation of the Box when the button is pressed

I'm using react.js, Typescript, chakra and animation framer.
There is a MotionSwipe component that allows you to swipe right and left. It can be dragged to swipe the Box of a child element.
We want the Box to move to the right or left not only by dragging, but also by pressing a button.
In the function onClickSwipe, which is called when the button is pressed, I executed the animateCardSwipe function to make it swipe to the right. However, the animation is too fast for me to see. I would like to slow down the animation of the Box moving to the right when the button is pressed.
import { Button,Box } from '#chakra-ui/react';
import { PanInfo, useMotionValue, useTransform } from 'framer-motion';
import { NextPage } from 'next';
import { MotionSwipe } from 'components/MotionSwipe';
import React, { useState } from 'react';
const Component: React.VoidFunctionComponent = () => {
const [cards, setCards] = useState([
{ text: 'Up or down', background: 'red' },
{ text: 'Left or right', background: 'green' },
{ text: 'Swipe me!', background: 'gray' },
]);
const [dragStart, setDragStart] = useState<{ axis: string | null; animation: { x: number; y: number } }>({
axis: null,
animation: { x: 0, y: 0 },
});
const x = useMotionValue(0);
const y = useMotionValue(0);
const onDirectionLock = (axis: string | null) => setDragStart({ ...dragStart, axis: axis });
const animateCardSwipe = (animation: { x: number; y: number }) => {
setDragStart({ ...dragStart, animation });
setTimeout(() => {
setDragStart({ axis: null, animation: { x: 0, y: 0 } });
x.set(0);
y.set(0);
setCards([...cards.slice(0, cards.length - 1)]);
}, 200);
};
const onDragEnd = (info: PanInfo) => {
if (dragStart.axis === 'x') {
if (info.offset.x >= 400) animateCardSwipe({ x: 300, y: 0 });
else if (info.offset.x <= -400) animateCardSwipe({ x: -300, y: 0 });
} else {
if (info.offset.y >= 100) animateCardSwipe({ x: 0, y: 100 });
else if (info.offset.y <= -100) animateCardSwipe({ x: 0, y: -100 });
}
};
const rotate = useTransform(x, [-700, 700], [-90, 90]);
const onClickSwipe = () => {
animateCardSwipe({ x: 500, y: 0 });
};
return (
<>
<Box display="flex" justifyContent="center" width="300px">
{cards.map((card, index) =>
index === cards.length - 1 ? (
<MotionSwipe
animate={dragStart.animation}
card={card}
key={index}
onDirectionLock={(axis: string) => onDirectionLock(axis)}
onDragEnd={(e: MouseEvent, info: PanInfo) => onDragEnd(info)}
style={{ x, y, zIndex: index, rotate: rotate }}
>
<Box background={card.background} height="300px" width="300px"></Box>
</MotionSwipe>
) : (
<MotionSwipe
card={card}
key={index}
style={{
zIndex: index,
}}
>
<Box background={card.background} height="300px" width="300px"></Box>
</MotionSwipe>
),
)}
</Box>
<Button onClick={onClickSwipe}>
○
</Button>
<Button>✖︎</Button>
</>
);
};
const SwipeBox: NextPage = () => {
return <Component />;
};
export default SwipeBox;
interface Props {
animate?: { x: number; y: number };
onDirectionLock?: (axis: 'x' | 'y') => void;
onDragEnd?: (e: MouseEvent, info: PanInfo) => void;
style: { x?: MotionValue; y?: MotionValue; zIndex: number; rotate?: MotionValue };
card: { text: string; background: string };
}
export const MotionSwipe: React.FunctionComponent<Props> = (props) => {
return (
<MotionBox
animate={props.animate}
background="white"
borderTopRadius="8px"
className="card"
display="grid"
drag
dragConstraints={{ left: 0, right: 0, top: 0, bottom: 0 }}
dragDirectionLock
left={0}
onDirectionLock={props.onDirectionLock}
onDragEnd={props.onDragEnd}
placeItems="center center"
position="absolute"
style={{ ...props.style }}
top={0}
transition={{ ease: [0.6, 0.05, -0.01, 0.9], duration: 3 }}
>
{props.children}
</MotionBox>
);
};
Yes, it's ok when it moves fast. How to change:
set useState variable to check if the button was pressed.
For example: const [pressed, setPressed] = useState(false);
If the button was pressed, you set setPressed(true)
If pressed(true) you add some transition or animation props with duration.
If you don't have duration, your element will triggers instantly after button click.

Resources