Slideshow effect with Framer Motion - reactjs

When some prop in my component changes in framer motion, I would like to fade that property out, then fade the "new" property value in?
Here's the timeline of the animation as I imagine it:
0: Prop Value Changes, old value start to fade out
.5: New value visible
1: New value finishes fading in
But the only way I see to do this with framer is to use Timeouts. Is there some way other than using timeouts to achieve this effect?
Codesandbox
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [seconds, setSeconds] = useState(0);
const [anim, setAnim] = useState("in");
useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => seconds + 1);
setAnim("in");
setTimeout(() => {
setAnim("out");
}, 500);
}, 1000);
return () => clearInterval(interval);
}, []);
const variants = {
out: {
opacity: 0
},
in: {
opacity: 1,
transition: {
duration: 0.5
}
}
};
return (
<motion.div
animate={anim}
variants={variants}
className="App"
style={{ fontSize: 100 }}
>
{seconds}
</motion.div>
);
}

You can do this with AnimatePresence.
Wrap your motion.div with an AnimatePresence tag, and use the seconds as a unique key for your div. The changing key will trigger AnimatePresence to animate the div in and out each time it changes (because new key means it's a different element).
To get this to work, you'll need to define your animations on the initial, animate, and exit props.
You'll also want to be sure to set the exitBeforeEnter prop on AnimatePresence so the fade out animation completes before the fade in starts.
Sandbox Example
export default function App() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<AnimatePresence exitBeforeEnter>
<motion.div
initial={{opacity:0}}
animate = {{opacity: 1, transition:{duration: 0.5}}}
exit={{opacity: 0 }}
className="App"
style={{ fontSize: 100 }}
key={seconds}
>
{seconds}
</motion.div>
</AnimatePresence>
);
}

Related

Framer Motion dynamic variants don't work when modifying initial properties

According to the docs I can make variant properties dynamic: https://www.framer.com/docs/animation/##dynamic-variants.
But this doesn't work when I try to make the initial properties dynamic.
For example:
import React, { useState, useEffect } from "react";
import { motion, useAnimation } from "framer-motion";
//make div appear from either bottom or right, depending on "origin" custom prop
const variant = {
hidden: (origin) =>
origin === "bottom"
? { x: 0, y: 200, opacity: 0 }
: { x: 200, y: 0, opacity: 0 },
visible: { x: 0, y: 0, opacity: 1, transition: { duration: 1 } },
};
function App() {
const [origin, setOrigin] = useState("bottom");
const controls = useAnimation();
//after 2 secs make origin "right"
useEffect(() => {
setTimeout(() => {
setOrigin("right");
}, 2000);
}, []);
//after 4 secs start the animation
useEffect(() => {
setTimeout(() => {
controls.start("visible");
}, 4000);
}, [controls]);
return (
<motion.div
style={{ width: 100, height: 50, background: "red" }}
variants={variant}
initial="hidden"
animate={controls}
custom={origin}
/>
);
}
export default App;
Here I made a dynamic variant to make a div appear from either the right or bottom, which I can control from a custom prop. Initially this custom prop is set to "bottom". After 2 secs, this is changed to "right". When I start the animation after 4 secs, I expect the div to appear from the right but it still appears from the bottom:
This is because the component is already rendered and is still the same component even if the origin prop being passed to the component has changed.
You can do two things:
Use a isVisible state variable where the render method will observe for changes and render the component when it becomes true.
function App() {
const [isVisible, setIsVisible] = useState(false);
...
//after 4 secs start the animation
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
controls.start("visible");
}, 4000);
}, [controls]);
return (
isVisible && (
<motion.div
...
/>
)
);
}
DEMO
Add a key prop to the component with the origin value so that when the value changes, React will re-render the component.
function App() {
...
return (
<motion.div
key={origin}
...
/>
);
}
DEMO
2nd option may be your preferred choice if you need to toggle between the origin.

How to change opacity with clickEvent in React?

I'm trying to change opacity of the span below so that I can show the user that the text is copied.
before copyToClipboard clicked => opacity: 0
after clicked => opacity: 1 for about 1 sec and again to opacity:0
I know onCopy={timer} wouldn't work but I really can't figure out how to approach.
import React, { useState, useEffect } from "react"
import { CopyToClipboard } from "react-copy-to-clipboard"
const Contact = () => {
const [style, setStyle] = useState({ opacity: 0 })
useEffect(() => {
const timer = setTimeout(function () {
setStyle({ opacity: 1 })
}, 1000)
}, [])
return (
<div>
<CopyToClipboard text='something' onCopy={timer}>
<LogoImage />
</CopyToClipboard>
<span style={style}>
copied!
</span>
</div>
I think you don't need useEffect for this case. Just create timer function outside useEffect like below:-
import React, { useState, useEffect } from "react"
import { CopyToClipboard } from "react-copy-to-clipboard"
const Contact = () => {
const [style, setStyle] = useState({ opacity: 0 })
const timer = () => {
setStyle({ opacity: 1 });
setTimeout(() => {
setStyle({ opacity: 0 });
}, 1000);
};
return (
<div>
<CopyToClipboard text='something' onCopy={() => timer()}>
<LogoImage />
</CopyToClipboard>
<span style={style}>
copied!
</span>
</div>
)
Instead of using setTimeout,you can use simple CSS animations to achieve the following -
Create a CSS animation using keyframes
Create a state to cause the animation and change state onCopy.
And that's it.
Check the code here, for ref-
https://codesandbox.io/s/festive-northcutt-kb32u?file=/src/App.js
I personally would not use timers for UI updates - ever. It does work (in this case), but it is not clean. I would suggest using CSS and a transition or animation. Removing/clearing the class name in onAnimationEnd makes sure the animation will trigger every time it is clicked. The animation duration is set to 1,4s out of which (0,2/1,4) = 14,29% are for fading in and 14,29% for fading out, that leaves 1s for the span to be shown
CSS:
.copied {
opacity: 0;
display:none;
}
.flash {
display:inline-block;
animation: flash 1.4s ease-in-out both;
}
#keyframes flash {
0: {
opacity: 0;
}
14.29% {
opacity: 1;
}
85.71% {
opacity: 1;
}
100% {
opacity: 0
}
}
(Simplified your example to remove the dependency on clipboard):
const Contact = () => {
const [flash, setFlash] = React.useState("")
const onClick = (event) => {
setFlash("flash");
}
const onAnimationEnd = (event) => {
setFlash("");
}
return (
<div onClick={onClick}>
Something
<span className={`copied ${flash}`} onAnimationEnd={onAnimationEnd}>
copied!
</span>
</div>
)
}
Like in your example this has the downside that the element has to exist all the time (even though now with display:none removed from rendering).
An even better approach would be to use the amazing TransitionGroup/CSSTransition from react-transition-group to add/remove the element. Admittedly, a bit much for this example, but in general the better and cleaner way to go.

GSAP Tween taking longer on each play in React

I am using GSAP's timeline to animate elements and it looks like it's taking longer and longer each time. In the example below, you can click on the box to animate it, and then click to reverse it. You can see in my setup that I don't have any delays set. If you open the console you will see the log takes longer and longer to execute the message in the onComplete function.
From research I've done, it looks like I am somehow adding a Tween, but I can't figure out how to solve this. Any help would be greatly appreciated. CodePen here.
const { useRef, useEffect, useState } = React
// set up timeline
const animTimeline = gsap.timeline({
paused: true,
duration: .5,
onComplete: function() {
console.log('complete');
}
})
const Box = ({ someState, onClick }) => {
const animRef = useRef();
animTimeline.to(animRef.current, {
x: 200,
})
useEffect(() => {
someState ? animTimeline.play() : animTimeline.reverse();
}, [someState])
return (
<div
className="box"
onClick={onClick}
ref={animRef}
>
</div>
)
}
const App = () => {
const [someState, setSomeState] = useState(false);
return(
<Box
someState={someState}
onClick={() => setSomeState(prevSomeState => !prevSomeState)}
/>
);
}
ReactDOM.render(<App />,
document.getElementById("root"))
Issue
I think the issue here is that you've the animTimeline.to() in the component function body so this adds a new tweening to the animation each time the component is rendered.
Timeline .to()
Adds a gsap.to() tween to the end of the timeline (or elsewhere using
the position parameter)
const Box = ({ someState, onClick }) => {
const animRef = useRef();
animTimeline.to(animRef.current, { // <-- adds a new tween each render
x: 200,
})
useEffect(() => {
someState ? animTimeline.play() : animTimeline.reverse();
}, [someState])
return (
<div
className="box"
onClick={onClick}
ref={animRef}
>
</div>
)
}
Solution
Use a mounting effect to add just the single tweening.
const animTimeline = gsap.timeline({
paused: true,
duration: .5,
onComplete: function() {
animTimeline.pause();
console.log('complete');
},
onReverseComplete: function() {
console.log('reverse complete');
}
})
const Box = ( { someState, onClick }) => {
const animRef = useRef();
useEffect(() => {
animTimeline.to(animRef.current, { // <-- add only one
x: 200,
});
}, []);
useEffect(() => {
someState ? animTimeline.play() : animTimeline.reverse();
}, [someState])
return (
<div
className="box"
onClick={onClick}
ref={animRef}
/>
)
};
Demo

useSpring won't animate on prop change

As opposed to what is mentioned in Spring's documentation, useSpring does not animate my counter component on prop change:
If you re-render the component with changed props, the animation will update.
I've tried passing the props as children, to no effect. What am I missing? Here's a demo:
https://codesandbox.io/s/spring-counter-jsylq?file=/src/App.js:199-230
import React, { useState } from "react";
import { animated, useSpring } from "react-spring";
const Counter = ({ value }) => {
const anim = useSpring({ from: { opacity: 0 }, to: { opacity: 1 } });
return <animated.h1 style={anim}>{value}</animated.h1>;
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<Counter value={count} />
<button onClick={() => setCount(count + 1)}>increment</button>
</>
);
}
It only said the animation will update. But now the animation is kind of static.
You can do a couple of things. If you add the reset property then it will repeat the initial animation at every re-render.
const anim = useSpring({ from: { opacity: 0 }, to: { opacity: 1 } , reset: true});
Or you can add some properties depending of the count value. For example the parity. Odd became blue, even number to red.
const anim = useSpring({ from: { opacity: 0 }, to: { opacity: 1 , color: value % 2 === 0 ? 'red' : 'blue'} });
What do you think?
https://codesandbox.io/s/spring-counter-forked-znnqk

useTransition mounts new object instantly

I am trying to figure out how to utilise useTransition for page transitions (simple opacity change where first page fades out and new one fades in).
So far I have this small demo going https://codesandbox.io/s/sleepy-knuth-xe8e0?file=/src/App.js
it somewhat works, but weirdly. When transition starts new page is mounted instantly while old one starts animating. This causes various layout issues and is not behaviour I am after. Is it possible to have first element fade out and only then mount and fade in second element?
Code associated to demo
import React, { useState } from "react";
import "./styles.css";
import { useTransition, a } from "react-spring";
export default function App() {
const [initial, setInitial] = useState(true);
const transition = useTransition(initial, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div>
{transition((style, initial) => {
return initial ? (
<a.h1 style={style}>Hello Initial</a.h1>
) : (
<a.h1 style={style}>Hello Secondary</a.h1>
);
})}
<button onClick={() => setInitial(prev => !prev)}>Change Page</button>
</div>
);
}
you can delay the start of the transition by waiting for the leave animation to complete.
const sleep = t => new Promise(res => setTimeout(res, t));
...
const transition = useTransition(initial, {
from: { position: "absolute", opacity: 0 },
enter: i => async next => {
await sleep(1000);
await next({ opacity: 1 });
},
leave: { opacity: 0 }
});
This delays the animation also for the very first time it is run. You can have a ref to keep track of whether the component has been rendered before or if it is its first time rendering, then you can skip sleep call if it's the first render.
OR
You can just simply provide trail config
const transition = useTransition(initial, {
from: { position: "absolute", opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
trail: 300
});
You need to add position: absolute and then you need to set the right position with css.
import React, { useState } from "react";
import "./styles.css";
import { useTransition, a } from "react-spring";
export default function App() {
const [initial, setInitial] = useState(true);
const transition = useTransition(initial, {
from: { position: 'absolute', opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div>
{transition((style, initial) => {
return initial ? (
<a.h1 style={style}>Hello Initial</a.h1>
) : (
<a.h1 style={style}>Hello Secondary</a.h1>
);
})}
<button onClick={() => setInitial(prev => !prev)}>Change Page</button>
</div>
);
}

Resources