Is it possible to animate to 100% in react-spring? - reactjs

I am using react-spring to try and animate in AJAX content as it is loaded.
I have a container component that I sometimes want to animate to 'auto' from 0 and sometimes I want to animate to 100% depending on a prop that is passed in.
I have a const that I set that is then passed into a calculatedHeight property in the Transition component. I then use this to set the height property in the mounted child component's style property.
const Container = ({ data, children, stretchHeight }) => {
const loaded = data.loadStatus === 'LOADED';
const loading = data.loadStatus === 'LOADING';
const animationHeight = stretchHeight ? '100%' : 'auto';
return (
<div
className={classnames({
'data-container': true,
'is-loading': loading,
'is-loaded': loaded,
'stretch-height': stretchHeight
})}
aria-live="polite"
>
{loading &&
<div style={styles} className='data-container__spinner-wrapper'>
<LoadingSpinner />
</div>
}
<Transition
from={{ opacity: 0, calculatedHeight: 0 }}
enter={{ opacity: 1, calculatedHeight: animationHeight }}
leave={{ opacity: 0, calculatedHeight: 0 }}
config={config.slow}
>
{loaded && (styles => {
return (
<div style={{ opacity: styles.opacity, height: styles.calculatedHeight }}>
{children}
</div>
)
}
)}
</Transition>
</div>
)
}
The problem is that this causes a max callstack exceeded error as I don't think react-spring can understand the '100%' string value, only 'auto'.
Is there a work around for this?

The problem is that you switch types, you go from 0 to auto to 0%. It can interpolate auto, but that gets interpolated as a number, you're going to confuse it by mixing that number with a percentage.
PS. Maybe you can trick a little using css: https://codesandbox.io/embed/xolnko178q

Thanks to #hpalu for helping me realise what the issue was:
The problem is that you switch types, you go from 0 to auto to 0%. It
can interpolate auto, but that gets interpolated as a number, you're
going to confuse it by mixing that number with a percentage.
To resolve this I created consts for both my start and end points.
const containerHeightAnimationStart = stretchHeight ? '0%' : 0;
const containerHeightAnimationEnd = stretchHeight ? '100%' : 'auto';
I then used these in the animation:
<Transition
native
from={{ opacity: 0, height: containerHeightAnimationStart }}
enter={{ opacity: 1, height: containerHeightAnimationEnd }}
leave={{ opacity: 0, height: containerHeightAnimationStart }}
>
{loaded && (styles => {
return (
<animated.div style={styles}>
{children}
</animated.div>
)
}
)}
</Transition>

from & to need same unit (number or string)
const [percentage, setPercentage] = useState(100);
// wrong
const animationState2 = useSpring({
from:{width: 0},
to: {width: `${percentage}%`}
});
// right
const animationState2 = useSpring({
from:{width: '0%'},
to: {width: `${percentage}%`}
});

Related

Animating height using Framer Motion not working in React

So, I have been trying to use Framer Motion for my React project. I basically want to animate the height from 0 to "auto", when the div gets rendered.
I tried the below code, but the height is not getting animated
<motion.div
initial={{ height: 0 }}
animate={{ height: "auto" }}
transition={{ duration: 0.5 }}
key={searchQuery?.length}
>
When I replaced height with width, the animation works fine, but can't figure out why the height is not getting animated. And I was unable to find any proper documentation regarding this.
Here is the CodeSandbox Link for demo.
Fixed versinn
What was wrong?
Your conditional rendering logic was in the wrong place, AnimatePresence works only if its direct child disappears.
exit prop was missing
key prop has to be stable, it can't be the length of the string
overflow: hidden have to be added so the children are invisible
Final code:
export default function App() {
const ref = useRef(null);
const [isActive, setIsActive] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>
<input
placeholder={"Enter Keyword"}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
}}
/>
<AnimatePresence>
{searchQuery?.length >= 1 && (
<motion.div
style={{ overflow: "hidden" }}
initial={{ height: 0 }}
animate={{ height: "auto" }}
transition={{ duration: 0.5 }}
exit={{ height: 0 }}
key={"container"}
>
{dataList?.map((listItem) => (
<div
style={{
padding: "1rem",
color: "#E090EE",
borderBottom: "1px solid #E1E1E1",
}}
>
{listItem.activity_title}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}

Animate input width from right to left framer motion

I'm trying to make my input appears with an animation making its width size going from 0 to 50%. For this, I've used framer-motion. I've managed to make the animation but by default it's growing from the left to the right. Is there any way I could make it change to have it growing from the right to the left ?
sandbox simple reproduction
import { motion } from "framer-motion";
import { useState } from "react";
export default function App() {
const [toggle, setToggle] = useState(false);
return (
<div>
<button onClick={() => setToggle(!toggle)}>toggle input</button>
<div style={{ marginTop: 25 }}>
{toggle && (
<motion.input
initial={{ width: "0%" }}
animate={{ width: "50%" }}
transition={{ duration: 1, origin: 1 }}
/>
)}
</div>
</div>
);
}
Pretty sure it's not the best way of doing this but I think changing the following should fix your problem. Added x with 50vw = 50% of viewport width (0vw = 0% width) for the initial "position" and for animate x with 0 for the ending position as well as width of 50vw.
<motion.input
initial={{ width: "0vw", x: "50vw" }}
animate={{ width: "50vw", x: 0 }}
transition={{ duration: 1, origin: 1 }}
/>
You can find more details about it on the API Documentation of the motion component under Value type conversion https://www.framer.com/docs/component/##value-type-conversion.

Initial animation on react-spring not working

I'm using react-spring to animate the transition of opening and closing an accordion component that reveals some text. Using this example on the documentation I was able to come up with a simpler version that creates a transition for the height and opacity:
function CollapseListItem({ title, text }: CollapseItemType) {
const [isOpen, setIsOpen] = useState(false);
const [ref, { height: viewHeight }] = useMeasure();
const { height, opacity } = useSpring({
from: { height: 0, opacity: 0 },
to: {
height: isOpen ? viewHeight : 0,
opacity: isOpen ? 1 : 0
}
});
const toggleOpen = () => {
setIsOpen(!isOpen);
};
return (
<>
<button onClick={toggleOpen}>
{title} click to {isOpen ? "close" : "open"}
</button>
<animated.div
ref={ref}
style={{
opacity,
height: isOpen ? "auto" : height,
overflow: "hidden"
}}
>
{text}
</animated.div>
</>
);
}
The issue is that the height transition is only being shown when you close the accordion, when you open the accordion the text suddenly appears, but on the code I can't seem to find why it only works on close, I've tried to hardcode some viewHeight values but I've had no luck.
Here's a code sandbox of what I have
After checking more examples, I realized that I was putting the ref on the wrong component, this change solve the issue:
function CollapseListItem({ title, text }: CollapseItemType) {
const [isOpen, setIsOpen] = useState(false);
const [ref, { height: viewHeight }] = useMeasure();
const props = useSpring({
height: isOpen ? viewHeight : 0,
opacity: isOpen ? 1 : 0
});
const toggleOpen = () => {
setIsOpen(!isOpen);
};
return (
<>
<button onClick={toggleOpen}>
{title} click to {isOpen ? "close" : "open"}
</button>
<animated.div
style={{
...props,
overflow: "hidden"
}}
>
<div ref={ref}>{text}</div>
</animated.div>
</>
);
}
Here's the full solution in case anyone is also trying to build an animated accordion / collapse using spring.

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