Using the library "# lottiefiles/react-lottie-player"
You need to get lottieRef to interact with animation, but I get null.
Code reference: https://codesandbox.io/s/great-rgb-7dp4j0?file=/src/HorizontalPicker/HorizontalPicker.jsx
import React, {useEffect, useRef} from "react";
import {Player} from "#lottiefiles/react-lottie-player";
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
There is also an interesting point.
If you output lottie, we get an object with null (while there is something inside it)
And if you output lottie.current, we get null.
Reference to an example of this thing: https://ibb.co/RQWxLkJ
Can you pass the lottie ref into your ref like the following (see ref on the div):
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current && lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
This way I am able to get the animation object when I console lottie.current
I'd prefer to use useState to save the animationData rather than a ref. But there was also an issue on the player where the 'load' event was firing but the player hadn't finish setting its internal instance of the animation therefor calling play() wouldn't work. This was happening only on functional components in React that's why it went undiscovered.
I've made a few changes to your code and to the player, using v1.5.2 you should be able to accomplish what you're looking for:
import css from "./HorizontalPicker.module.css";
import React, { useRef, useState, useEffect } from "react";
import { Player } from "#lottiefiles/react-lottie-player";
const HorizontalPicker = () => {
const player = useRef(null);
// const lottie = useRef(null);
const [lottie, setLottie] = useState(null);
useEffect(() => {
if (lottie) {
console.log(" Lottie animation data : ");
console.log(lottie);
// You can also play by calling the underlying lottie method
// lottie.play();
}
}, [lottie]);
return (
<>
<Player
onEvent={(event) => {
// console.log(event);
if (event === "instanceSaved" && player && player.current) {
console.log("Playing animation..");
player.current.play();
}
}}
lottieRef={(data) => {
setLottie(data);
}}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"
}
/>
</>
);
};
export default HorizontalPicker;
Sandbox:
https://codesandbox.io/s/lottie-react-functional-component-2bs8fs?file=/src/HorizontalPicker/HorizontalPicker.jsx
Cheers!
Well, according to official documentation https://github.com/LottieFiles/lottie-react, lottieRef represents a callback function which is fired by Player component (and this function returns AnimationFrame object)
I'm not familiar with this library, and whatever I'll describe next are just my assumptions :) Seems that whenever player "plays", it displays frames one-by-one (from json file in "src" attribute"). And whenever player displays frames from .json file - Player fires "lottieRef" event which you utilize to set lottie.current. And player starts displaying frames only when it loads .json data using "src" parameter in Player definition (see network tab in your browser to ensure that additional http request presents)
And in this case everything seems pretty logical: you try to access "lottie" variable in useEffect of your component but it's empty as far as Player did not manage to display any frame yet because the callback (lottieRef) did not fire yet as far as .json file is not loaded yet. No matter if .json file is large or small, Player requests it via additional http call, which requires some (even minital) amount time. And useEffect fires before .json is loaded immediately after rendering DOM (that's how ReactJS works)
On the other hand, if you delay a bit request to "lottie" ref - you'll see that it is populated:
Code example:
import React, { useEffect, useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
useEffect(() => {
setTimeout(() => console.log(lottie), 50);
}, []);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
So if you delay onEvent callback event a bit, "lottie" ref would be initialized by that moment and .play() would work:
onEvent={(event) => {
event === 'load' &&
setTimeout(
() => lottie.current && lottie.current.play()
);
}
}
BUT, if the only purpose you have is to execute Player whenever it's ready - why not to use "autoplay" built-in property ? (autoplay={true})
import React, { useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={true}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
Hope it'll help
Made a smoothscroll component using framer motion that's working well :
export default function SmoothScroll({ children }: Props) {
const { width } = useWindowSize();
const scrollContainer = useRef() as RefObject<HTMLDivElement>;
const [pageHeight, setPageHeight] = useState(0);
useEffect(() => {
setTimeout(() => {
// added a setTimeout so the page has the time to load and it still fits
const scrollContainerSize =
scrollContainer.current?.getBoundingClientRect();
scrollContainerSize && setPageHeight(scrollContainerSize.height);
}, 500);
}, [width]);
const { scrollY } = useScroll(); // measures how many pixels user has scrolled vertically
// as scrollY changes between 0px and the scrollable height, create a negative scroll value...
// ... based on current scroll position to translateY
const transform = useTransform(scrollY, [0, pageHeight], [0, -pageHeight]);
const physics = { damping: 15, mass: 0.17, stiffness: 55 }; // easing of smooth scroll
const spring = useSpring(transform, physics); // apply easing to the negative scroll value
return (
<>
<motion.div
ref={scrollContainer}
style={{ y: spring }} // translateY of scroll container using negative scroll value
className="app fixed overflow-hidden w-screen"
>
{children}
</motion.div>
<motion.div style={{ height: pageHeight }} />
</>
);
}
The thing is, I'd like to scrollTo sections of my page upon click on the navbar but don't really know how to implement it without removing the smoothScroll ...
Tried the following logic but obviously it did not work as the vanilla scroll has been hijacked :
const scrollToSection = (
e: React.MouseEvent<HTMLLIElement, globalThis.MouseEvent>,
anchor?: string
) => {
e.preventDefault();
if (!anchor) return;
const section = document.querySelector(anchor);
section?.scrollIntoView({ behavior: "smooth" });
};
Is it doable ?
My question is fairly simple but i'm struggling to find the solution. Here i'm trying to create a staggering animation for each letter in a sentence, i want to use the y axis as a parameter to animate but i'm not getting the result i wanted as the sentence fully mounted without animating. But when i tried to use opacity as a parameter, it works exactly fine. What did i do wrong here? Any help would be appreciated :)
// import "./styles.css";
import { motion } from "framer-motion";
export default function App() {
const quotes = "Hello World.";
const parent = {
animate: {
transition: {
staggerChildren: 0.1,
},
},
};
const child = {
initial: { y: 400 },
animate: {
y: 0,
},
};
return (
<div>
<motion.div variants={parent} initial='initial' animate='animate'>
{
quotes.split('').map((item, index) => (
<motion.span variants={child} key={index}>{item}</motion.span>
))
}
</motion.div>
</div>
);
}
To help you see my problem, here is codesandbox example
https://codesandbox.io/s/busy-mountain-fv58xc?file=/src/App.js:0-620
Animating x and y doesn't work for <span> because it's and inline element. It flows with the content and doesn't have an explicit x and y position to animate.
You can change your spans to a block-level element (like div), or you could add some styling to tell the spans to display as blocks:
<motion.span style={{display: "inline-block"}} variants={child} key={index}>
{item}
</motion.span>
I'm trying to achieve callback-based route transitions using Next.js's framework and Greensock animation library (if applicable). For example when I start on the homepage and then navigate to /about, I want to be able to do something like:
HomepageComponent.transitionOut(() => router.push('/about'))
ideally by listening to the router like a sort of middleware or something before pushing state
Router.events.on('push', (newUrl) => { currentPage.transitionOut().then(() => router.push(newUrl)) });
Main Problem
The main problem is that I also have a WebGL app running in the background, decoupled from the React ecosystem (since it uses requestAnimationFrame). So the reason I want callback-based transitions is because I need to run them after the WebGL transitions are done.
Current Implementation
I've looked into using React Transition Group and I've seen the docs for the Router object but neither seems to be callback-based. In other words, when I transition to a new page, the WebGL and the page transitions run at the same time. And I don't want to do a hacky solution like adding a delay for the page transitions so they happen after the WebGL ones.
This is what I have right now:
app.js
<TransitionGroup>
<Transition
timeout={{ enter: 2000, exit: 2000 }}
// unmountOnExit={true}
onEnter={(node) => {
gsap.fromTo(node, { opacity: 0 }, { opacity: 1, duration: 1 });
}}
onExit={(node) => {
gsap.to(node, { opacity: 0, duration: 1 });
}}
key={router.route}
>
<Component {...pageProps}></Component>
</Transition>
</TransitionGroup>
webgl portion
Router.events.on('routeChangeStart', (url) => {
// transition webGL elements
// ideally would transition webGL elements and then allow callback to transition out html elements
});
I've also tried using the eventemitter3 library to do something like:
// a tag element click handler
onClick(e, href) {
e.preventDefault();
this.transitionOut().then(() => { Emitter.emit('push', href); });
// then we listen to Emitter 'push' event and that's when we Router.push(href)
}
However this method ran into huge issues when using the back / forward buttons for navigating
Bit late on this but I was looking into this myself today. It's really easy to use Framer Motion for this but I also wanted to use GSAP / React Transition Group.
For Framer Motion I just wrapped the Next < Component > with a motion component:
<motion.div
key={router.asPath}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<Component {...pageProps} />
</motion.div>
For GSAP / React Transition Group, not sure if this is the right way but it's working as intended for me (see comments):
const [state, setstate] = useState(router.asPath) // I set the current asPath as the state
useEffect(() => {
const handleStart = () => {
setstate(router.asPath) // then on a router change, I'm setting the state again
// other handleStart logic goes here
}
const handleStop = () => {
... // handleStop logic goes here
}
router.events.on("routeChangeStart", handleStart)
router.events.on("routeChangeComplete", handleStop)
router.events.on("routeChangeError", handleStop)
return () => {
router.events.off("routeChangeStart", handleStart)
router.events.off("routeChangeComplete", handleStop)
router.events.off("routeChangeError", handleStop)
}
}, [router])
<Transition
in={router.asPath !== state} // here I'm just checking if the state has changed, then triggering the animations
onEnter={enter => gsap.set(enter, { opacity: 0 })}
onEntered={entered => gsap.to(entered, { opacity: 1, duration: 0.3 })}
onExit={exit => gsap.to(exit, { opacity: 0, duration: 0.3 })}
timeout={300}
appear
>
<Component {...pageProps} />
</Transition>
First I recommend reading Greensock’s React documentation.
Intro Animations in Next.JS
For intro animations, if you use useLayoutEffect with SSR your console will fill up with warnings. To avoid this apply useIsomorphicLayoutEffect instead. Go to useIsomorphicLayoutEffect.
To prevent the flash of unstyled content (FOUC) with SSR, you need to set the initial styling state of the component. For example, if we are fading in, the initial style of that component should be an opacity of zero.
Outro Animations in Next.JS
For outro animations, intercept the page transition, and do the exit animations, then onComplete route to the next page.
To pull this off, we can use TransitionLayout higher order component as a wrapper to delay the routing change until after any animations have completed, and a TransitionProvider component that will take advantage of React’s useContext hook to share an outro timeline across multiple components, regardless of where they are nested.
Transition Context
In order to make a page transition effect, we need to prevent rendering the new page before our outro animation is done.
We may have many components with different animation effects nested in our pages. To keep track of all the different outro transitions, we will use a combination of React’s Context API and a top-level GSAP timeline.
In TransitionContext we will create our TransitionProvider which will make our GSAP timeline for outro animations available to any components who would like to transition out during a page change.
import React, { useState, createContext, useCallback } from "react"
import gsap from "gsap"
const TransitionContext = createContext({})
const TransitionProvider = ({ children }) => {
const [timeline, setTimeline] = useState(() =>
gsap.timeline({ paused: true })
)
return (
<TransitionContext.Provider
value={{
timeline,
setTimeline,
}}
>
{children}
</TransitionContext.Provider>
)
}
export { TransitionContext, TransitionProvider }
Next, we have TransitionLayout which will be our controller that will initiate the outro animations and update the page when they are all complete.
import { gsap } from "gsap"
import { TransitionContext } from "../context/TransitionContext"
import { useState, useContext, useRef } from "react"
import useIsomorphicLayoutEffect from "../animation/useIsomorphicLayoutEffect"
export default function TransitionLayout({ children }) {
const [displayChildren, setDisplayChildren] = useState(children)
const { timeline, background } = useContext(TransitionContext)
const el = useRef()
useIsomorphicLayoutEffect(() => {
if (children !== displayChildren) {
if (timeline.duration() === 0) {
// there are no outro animations, so immediately transition
setDisplayChildren(children)
} else {
timeline.play().then(() => {
// outro complete so reset to an empty paused timeline
timeline.seek(0).pause().clear()
setDisplayChildren(children)
})
}
}
}, [children])
return <div ref={el}>{displayChildren}</div>
}
In a custom App component, we can have TransitionProvider and TransitionLayout wrap the other elements so they can access the TransitionContext properties. Header and Footer exist outside of Component so that they will be static after the initial page load.
import { TransitionProvider } from "../src/context/TransitionContext"
import TransitionLayout from "../src/animation/TransitionLayout"
import { Box } from "theme-ui"
import Header from "../src/ui/Header"
import Footer from "../src/ui/Footer"
export default function MyApp({ Component, pageProps }) {
return (
<TransitionProvider>
<TransitionLayout>
<Box
sx={{
display: "flex",
minHeight: "100vh",
flexDirection: "column",
}}
>
<Header />
<Component {...pageProps} />
<Footer />
</Box>
</TransitionLayout>
</TransitionProvider>
)
}
Component-Level Animation
Here is an example of a basic animation we can do at the component level. We can add as many of these as we want to a page and they will all do the same thing, wrap all its children in a transparent div and fade it in on page load, then fade out when navigating to a different page.
import { useRef, useContext } from "react"
import { gsap } from "gsap"
import { Box } from "theme-ui"
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
import { TransitionContext } from "../context/TransitionContext"
const FadeInOut = ({ children }) => (
const { timeline } = useContext(TransitionContext)
const el = useRef()
// useIsomorphicLayoutEffect to avoid console warnings
useIsomorphicLayoutEffect(() => {
// intro animation will play immediately
gsap.to(el.current, {
opacity: 1,
duration: 1,
})
// add outro animation to top-level outro animation timeline
timeline.add(
gsap.to(el.current, {
opacity: 1,
duration: .5,
}),
0
)
}, [])
// set initial opacity to 0 to avoid FOUC for SSR
<Box ref={el} sx={{opacity: 0}}>
{children}
</Box>
)
export default FadeInOut
We can take this pattern and extract it into an extendable AnimateInOut helper component for reusable intro/outro animation patterns in our app.
import React, { useRef, useContext } from "react"
import { gsap } from "gsap"
import { Box } from "theme-ui"
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
import { TransitionContext } from "../context/TransitionContext"
const AnimateInOut = ({
children,
as,
from,
to,
durationIn,
durationOut,
delay,
delayOut,
set,
skipOutro,
}) => {
const { timeline } = useContext(TransitionContext)
const el = useRef()
useIsomorphicLayoutEffect(() => {
// intro animation
if (set) {
gsap.set(el.current, { ...set })
}
gsap.to(el.current, {
...to,
delay: delay || 0,
duration: durationIn,
})
// outro animation
if (!skipOutro) {
timeline.add(
gsap.to(el.current, {
...from,
delay: delayOut || 0,
duration: durationOut,
}),
0
)
}
}, [])
return (
<Box as={as} sx={from} ref={el}>
{children}
</Box>
)
}
export default AnimateInOut
The AnimateInOut component has built in flexibility for different scenarios:
Setting different animations, durations and delays for intros and outros
Skipping the outro
Setting the element tag for the wrapper, e.g. use a <span> instead of a <div>
Use GSAP’s set option to define initial values for the intro
Using this we can create all sorts of reusable intro/outro animations, such as <FlyInOut>, <ScaleInOut>, <RotateInOut3D> and so forth.
I have a demo project where you can see the above in practice: TweenPages
Following simple component from the official examples:
import {useSpring, animated} from 'react-spring'
function App() {
const props = useSpring({opacity: 1, from: {opacity: 0}})
return <animated.div style={props}>I will fade in</animated.div>
}
Question
How do I animate the fadeIn-effect (or any other animation) again for example when I click on a button or when a promise is resolved?
You can basically make two effect with useSpring and an event.
You can change the style for example the opacity depending on the state of an event.
You can restart an animation on state change. The easiest way to restart is to rerender it.
I created an example. I think you want the second case. In my example I rerender the second component with changing its key property.
const Text1 = ({ on }) => {
const props = useSpring({ opacity: on ? 1 : 0, from: { opacity: 0 } });
return <animated.div style={props}>I will fade on and off</animated.div>;
};
const Text2 = () => {
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
return <animated.div style={props}>I will restart animation</animated.div>;
};
function App() {
const [on, set] = React.useState(true);
return (
<div className="App">
<Text1 on={on} />
<Text2 key={on} />
<button onClick={() => set(!on)}>{on ? "On" : "Off"}</button>
</div>
);
}
Here is the working example: https://codesandbox.io/s/upbeat-kilby-ez7jy
I hope this is what you meant.