Framer Motion animation resetting when menu is opened - reactjs

I have a fancy heading animation using whileInView to trigger, but when I open my menu drawer, the heading animations all reset to the initial state where they are all hidden.
The heading animation function firstly splits up a string into spans containing each word then wraps each letter in a span and nudges the letters down to animate them in one at a time.
It works great, but whenever I click the hamburger, the animated headings all revert to translateY(100%) and won't reveal back even though I have set viewport={{ once: true }}.
Here is my code:
function AnimatedHeading(props) {
const {
text,
stagger = 0.015,
delay = 0.5
} = props || {}
// Word wrapper
const WordWrap = (props) => {
return <span className="whitespace-nowrap">{props.children}</span>;
}
// Breaking string into array of words
const splitWords = text.split(" ");
const words = [];
for (const [, item] of splitWords.entries()) {
words.push(item.split(""));
}
// Add a space ("\u00A0") to the end of each word
words.map((word) => {
return word.push("\u00A0");
});
const variants = {
hidden: {
y: '100%',
transition: { ease: [0.455, 0.03, 0.515, 0.955], duration: 0.85 }
},
animate: {
y: '0%',
transition: { ease: [0.455, 0.03, 0.515, 0.955], duration: 0.75 }
}
};
return (
<AnimatePresence>
<span className="sr-only">{text}</span>
<motion.span
key={`text`}
className="inline-block"
initial="hidden"
whileInView='animate'
viewport={{ once: true }}
transition={{
delayChildren: delay,
staggerChildren: stagger
}}
>
{words.map((word, n) => (
<WordWrap key={n}>
{word.flat().map((char, index) => (
<span key={index} className="overflow-hidden pb-1 inline-block">
<motion.span
key={`animated-heading-${char}-${index}`}
variants={variants}
viewport={{ once: true }}
className="relative inline-block overflow-visible"
>
{char}
</motion.span>
</span>
))}
</WordWrap>
))}
</motion.span>
</AnimatePresence>
)
}
I have tried many different combinations of this, but can't seem to get this bug fixed. Any help appreciated. I'm pretty new to Framer Motion, so I'm sort of learning it as I go.

Related

Framer-motion: Fire onTap() when click the other button

I have RotatingButton which is turning and changing scale when clicked.
(Rotating has to be slower so I used two motions and added duration to rotate.)
Button is adding +1 to state. ( addValue() )
<div>
<div>
<motion.button
onClick={addValue}
animate={{ rotate: rotation }}
transition={{ duration: 0.5, ease: 'easeIn' }}
>
<motion.div whileTap={{ scale: 0.8 }}>
<RotatingButton />
</motion.div>
</motion.button>
</div>
<div>
<button onClick={removeValue}>{text}</button>
</div>
</div>
But the problem is that I have second button which is removing -1 value from state ( removeValue() ) and it also has to turn and scale the RotatingButton.
Rotating is easy, I added dependency:
const [rotation, setRotation] = useState<number>(0);
and setting rotation on each addValue() and removeValue().
setRotation(rotation + 180);
but the problem is that I couldn't find the way to scale RotatingButton for 0.5 sec for example by triggering whileTap() on when removeValue() is triggered.
I've tried to make scale state and use useEffect nad trigger it when rotation is changing:
useEffect(() => {
setScale(0.8);
setTimeout(() => {
setScale(0);
}, 300);
}, [rotation]);
and I've also tried
const controls = useAnimationControls();
with controls.start, controls.mount etc.
To trigger it other way, with animation to change scale for 0.5 sec when changing the state, skipping whileTap.
I've tried onTapStart() too.
None of that worked.
Please help me.
The way was to use sequence
https://www.framer.com/motion/use-animation-controls/#sequence
const controls = useAnimationControls();
const sequence = async () => {
await controls.start({ scale: 0.8, transition: { duration: 0.5 } });
return await controls.start({ scale: 1, transition: { duration: 0.5 } });
};
and change whileTap() to animate={controls}
<motion.button
onClick={addValue}
animate={{ rotate: rotation }}
transition={{ duration: 0.5, ease: 'easeIn' }}
>
<motion.div animate={controls}>
<RotatingButton />
</motion.div>
</motion.button>

When new items are added, the first existing items are not animated using framer motion

When there are only two items initially, clicking Add will add new item (3rd item). The 2nd item will go below and show a heading. Going below is not animated.
In the same way, when there are 3 items and clicked Delete, the last movement is not animated.
Demo with full source can be found here: https://codesandbox.io/s/practical-dream-229d2y?file=/src/App.tsx
This is the source code for archive purposes:
import { useState } from "react";
import { AnimatePresence, AnimateSharedLayout, motion } from "framer-motion";
function SomeItem() {
return (
<div
style={{
border: "1px solid blue",
padding: "12px",
height: 60,
width: "100%",
margin: "12px"
}}
>
{new Date().toISOString()}
</div>
);
}
export default function App() {
const [items, setItems] = useState<typeof SomeItem[]>([SomeItem, SomeItem]);
return (
<div className="App">
<button
onClick={() =>
setItems((prev) => prev.filter((_p, i) => i !== prev.length - 1))
}
>
Delete
</button>
<button onClick={() => setItems((prev) => [...prev, SomeItem])}>
Add
</button>
<section>
<h3>What&apos;s not working?</h3>
<p>
When there are only two items initially, clicking Add will add new
item (3rd item). The 2nd item will go below and show a heading. Going
below is not animated.
</p>
<p>
In the same way, when there are 3 items and clicked Delete, the last
movement is not animated.
</p>
</section>
<AnimateSharedLayout>
<ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
<AnimatePresence initial>
{items.map((Item, i) => (
<motion.li
key={i}
layout
animate={{ transition: { damping: 1000 }, y: 0 }}
exit={{ opacity: 0, transition: { damping: 1000 }, y: "-110%" }}
initial={{ y: "-110%" }}
>
{i !== 0 && items.length > 2 && (
<motion.h3
animate={{ transition: { damping: 0 }, x: 0 }}
exit={{ x: "-100%" }}
initial={{ x: "-100%" }}
>
Item {i}
</motion.h3>
)}
<Item />
</motion.li>
))}
</AnimatePresence>
</ul>
</AnimateSharedLayout>
</div>
);
}
The problem seems to be at this conditional i !== 0 && items.length > 2, when the items.length is greater than two it's rendering two headers at same time because you specified to only render the header after the item of index 0, thus when you delete the item it's also deleting the two headers at same time and just animating the last one. To fix it you can change the conditional to i !== 0 && items.length > 1, this way it will render item 1, item 2 and so on. see the working example

react-spring transition is not showing when first time rendering

I am using React-spring for first time. I am trying to use transition hook on a side drawer on my page by toggling a button.
But when I am clicking on that button there is no animation as that side drawer opens instantly, but if I click second time then side drawer is closing with animation.
And also if I click that button before that drawer removed from DOM then slide from left animation is there. I can't figure it out where is the problem. Help me please. Thanks.
Here is my code:
import React, { useState } from "react";
import { useTransition, animated, config } from "react-spring";
const Transform = (props) => {
const myStyle = {
position: "fixed",
left: 0,
top: 0,
zIndex: 100,
backgroundColor: "black",
};
const [drawerIsOpen, setDrawerState] = useState(false);
const closeDrawerHandler = () => {
setDrawerState((v) => !v);
};
const transition = useTransition(drawerIsOpen, {
form: { transform: "translateX(-100%)", opacity: 0 },
enter: { transform: "translateX(0%)", opacity: 1 },
leave: { transform: "translateX(-100%)", opacity: 0 },
config: { duration: 2000 },
// config: config.molasses,
// openDrawerHandler: () => setDrawerState(true),
});
return (
<>
{transition((style, item) =>
item ? (
<animated.aside
className='bg-white h-100 w-70 shadow'
style={{ ...style, ...myStyle }}
onClick={closeDrawerHandler}
>
<nav className='h-100'>
<h2>It's a Side Drawer</h2>
</nav>
</animated.aside>
) : (
""
)
)}
<div className='d-flex justify-content-end'>
<button className='btn btn-primary ' onClick={closeDrawerHandler}>
Toggle Btn
</button>
</div>
</>
);
};
export default Transform;
Image of transition problem
I made spelling mistake. Silly one, I spelt from as form . That's why it was happening.

Avoiding framer-motion initial animations on mount

Please see this codesandbox.
I have a basic framer-motion animation where the height of a box is animated when toggled. However, I want the box to be shown by default, but when the page loads the initial animation is presented.
My question is, how do I avoid having an initial animation for a component if it should be shown on mount, but still maintain future enter and exit animations? Thanks!
I came up with this kind of solution;
1- I took variants to inside of the component
2 - I created two states for opacity and height
3 - States are initially same as where you animate to. So basically nothing happens when you first render.
4 - With useEffect, you can swap the values with the actual initial values, so after first render, the animation works.
export const AnimatedFallback = ({ isVisible }) => {
const [opacity, setOpacity] = useState(1);
const [height, setHeight] = useState("200px");
const variants = {
initial: {
opacity: opacity,
height: height
},
enter: {
opacity: 1,
height: "200px",
transition: { duration: 0.5 }
},
exit: {
opacity: 0,
height: 0,
transition: { duration: 0.5 }
}
};
useEffect(()=> {
setHeight(0)
setOpacity(0)
}, [])
return (
<AnimatePresence>
{isVisible && (
<motion.div
animate="enter"
className="fallback"
exit="exit"
initial="initial"
variants={variants}
>
Suspense Fallback Component
</motion.div>
)}
</AnimatePresence>
);
};
You can check if it is the components first render by:
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
});
If it is the first render of the component, don't pass the variants to the motion.div. In your example this would look like this:
const variants = {
initial: {
opacity: 0,
height: 0
},
enter: {
opacity: 1,
height: "200px",
transition: { duration: 0.5 }
},
exit: {
opacity: 0,
height: 0,
transition: { duration: 0.5 }
}
};
export const AnimatedFallback = ({ isVisible }) => {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
});
return (
<AnimatePresence>
{isVisible && (
<motion.div
animate="enter"
className="fallback"
exit="exit"
initial="initial"
variants={firstRender.current ? {} : variants}
>
Suspense Fallback Component
</motion.div>
)}
</AnimatePresence>
);
};
Bit old of a question, but just in case some people stumble upon this. if you are using an AnimatePresence you can use initial={false} on the AnimatePresence component. Like so,
<AnimatePresence initial={false}>
{someCondition ? (
<motion.h1 {...yourProps}>
yooo
</motion.h1>
) : null}
</AnimatePresence>
More info here:
https://www.framer.com/docs/animate-presence/##suppressing-initial-animations
The simplest method is provided in the framer motion official docs:
specifiy the initial prop with the value as false
<motion.div
...
initial={false}
>
</motion.div>
https://www.framer.com/docs/animation/##enter-animations
For me specifying initial property in motion.div helped
<motion.div initial={{ opacity: 0, height: 0}}>
...
</motion.div>

How can i switch between react components using framer motion?

In my react app i need to switch between components like in a carousel. I found this example to build an image carousel only using framer motion: https://codesandbox.io/s/framer-motion-image-gallery-pqvx3?file=/src/Example.tsx:1715-1725
I want to adapt this to switching between components. At the moment my page looks something like this:
const variants = {
enter: (direction: number) => {
return {
x: direction > 0 ? 100 : -100,
opacity: 0,
}
},
center: {
zIndex: 1,
x: 0,
opacity: 1,
},
exit: (direction: number) => {
return {
zIndex: 0,
x: direction < 0 ? 100 : -100,
opacity: 0,
}
},
}
const Page = () => {
const [[page, direction], setPage] = useState([0, 0])
const paginate = (newDirection: number) => {
setPage([page + newDirection, newDirection])
}
return (
<motion.div
key={page}
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
>
<!-- my components, between which I want to switch, should appear here -->
</motion.div>
)
}
how would I have to build the logic to be able to switch dynamic between my components (slides)? In the codesandbox example the images were changed via an array:
const imageIndex = wrap(0, images.length, page);
<motion.img key={page} src={images[imageIndex]} />
How could i do that to switch between jsx elements?
Edit
The answer from Joshua Wootonn is correct, but you need to add the custom prop also to the TestComp to get the animation working with dynamic variants like this:
const TestComp = ({ bg }: { bg: string }) => (
<motion.div
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{
x: { type: "spring", stiffness: 100, damping: 30 },
opacity: { duration: 0.2 },
}}
className="absolute w-full h-full"
style={{
background: bg,
}}
/>
)
A couple of things were missing from the above answer to get exit animations working.
If you want exit animations to work within AnimationPresense you need to set keys on its children
<AnimatePresence initial={false} custom={direction}>
{page === 0 && <TestComp key="0" bg="rgb(171, 135, 255)" />}
{page === 1 && <TestComp key="1" bg="rgb(68, 109, 246)" />}
{page === 2 && <TestComp key="2" bg="rgb(172, 236, 161)" />}
</AnimatePresence>
If you want to animate something in while something is still animating out without having massive content shifting, you need to take them out of the flow. (use absolute positioning and wrap with relatively positioned container)
<div style={{ position: "relative", height: "300px", width: "300px" }}>
<AnimatePresence initial={false} custom={direction}>
...
</AnimatePresence>
</div>
and on the child components
height: 100%;
width: 100%;
position: absolute;
Working codesandbox: https://codesandbox.io/s/framer-motion-carousel-animation-wetrf?file=/src/App.tsx:658-708
Your components should return <motion.div> (or <motion.section>, <motion.span> etc.).
And in the page component you should use <AnimatePresence /> component (like in the example):
<AnimatePresence initial={false} custom={direction}>
{COMPONENTS}
</AnimatePresence>
Then you have to decide which component will appear:
{page === 0 && <ComponentOne />}
{page === 1 && <ComponentTwo/>}
{page === 2 && <ComponentThree/>}
The animations you can control with variants.
You can see a quick demo here: https://codesandbox.io/s/quizzical-hypatia-7wqjc?file=/src/App.tsx

Resources