[BUG] Animate does not honor `autoplay: false`
When adding opacity in a animate() animation, it will always animate immediately when this function is called, no matter if autoplay: false is set or not.
Using [0, 1] as opacity styles instead of only 1 does not make a difference here.
Opting out of WAAPI by setting repeatDelay: 0.0001 does seem to make it work as expected.
Reproduction
https://fefyi.dev/zox1a3s09up
Steps to reproduce
Steps to reproduce the behavior:
- Go to sandbox
- Notice the element immediately jumping to opacity 0.5, before the translate x starts 2 seconds later
Expected behavior
The initial styles are set to be opacity 0. One would expect the element to stay opacity 0, until after the 2 second timeout. Then both opacity and X should animate at the same time.
7. Environment details
motion/[email protected]. Yet also happens in older versions.
Also noticed that when adding the repeatDelay while setting the initial opacity to 0, the starting value is never exactly zero. Often a very small number like 0.05. But in case of opacity this is a problem, because you see the element.
Made an additional sandbox where we use repeatDelay and an initial opacity of 0 here: https://fefyi.dev/0i8yf8eryaf
Still happening today with version 12.16.0
You are currently at your concurrent task limit: 5. Either wait for a task to finish, or pause a task in the jules.google.com UI and then reassign this issue.\n\n\nPlease check out https://jules.google/docs/usage-limits for more inotmation on rate limits.
It's a bug indeed, but there's a simple workaround we can use, which is delaying the whole animate function!
"use client";
import { useEffect } from "react";
import { useAnimate } from "motion/react";
export default function Motion() {
const [scope, animate] = useAnimate();
useEffect(() => {
// WORKAROUND: Motion has a bug where opacity doesn't respect autoplay: false
// due to CSS reconciliation issues. When Motion detects CSS opacity-0 class,
// it immediately tries to reconcile the state, causing opacity to animate
// immediately while transform properties correctly wait for autoplay.
//
// SOLUTION: Simply delay the entire animate call to avoid the reconciliation
// bug entirely. This is cleaner than setting initial state separately.
//
// Animation flow:
// 1. (initial render) CSS opacity-0 applied, element is hidden
// 2. (after 3 seconds) Motion animates both opacity and transform together
// 3. No immediate reconciliation = both properties behave consistently
setTimeout(() => {
animate("h1", { opacity: 1, x: 200 }, { duration: 5 });
}, 3000);
}, []);
return (
<div ref={scope} className="grid min-h-dvh place-items-center text-white">
<div className="flex flex-col items-center">
<img
className="mb-8 h-12 w-12"
src="https://sandpack.frontend.fyi/img/fefyi.svg"
/>
{/* CSS opacity-0 for initial render, Motion will animate from this state */}
<h1 className="mb-6 max-w-[60%] text-center font-mono text-3xl text-balance text-white opacity-0">
Time to draw some rectangles
</h1>
<a
href="https://www.frontend.fyi/dev"
target="_blank"
className="text-sm uppercase"
>
Frontend.FYI Dev Playgrounds
</a>
</div>
</div>
);
}
Hey @Kareem-AEz ! Thanks so much for also looking into this! Unfortunately the bug exists if you combine it with autoplay: false. This is a property I like to use a lot in my animations, since you can easily make animations go forwards/backwards based on for example scroll or click events.
As you see in this new example, the animation still runs when the timeout ends, and it ignores the autoplay property: https://fefyi.dev/75xgg6s8td
Did you already manage to have a look at this @mattgperry?
Hey everyone,
I'm taking a look in the source files to try and fix this autoplay bug. I did a couple of tests and I think we can easily solve this by adding autoplay to the options of the supportsBrowserAnimation function in the waapi.ts file. Full path /motion-main/packages/motion-dom/src/animation/waapi/supports/waapi.ts.
export function supportsBrowserAnimation(
options: ValueAnimationOptionsWithRenderContext
) {
const {
motionValue,
name,
repeatDelay,
repeatType,
damping,
type,
autoplay,
} = options
const subject = motionValue?.owner?.current
/**
* We use this check instead of isHTMLElement() because we explicitly
* **don't** want elements in different timing contexts (i.e. popups)
* to be accelerated, as it's not possible to sync these animations
* properly with those driven from the main window frameloop.
*/
if (!(subject instanceof HTMLElement)) {
return false
}
const { onUpdate, transformTemplate } = motionValue!.owner!.getProps()
return (
supportsWaapi() &&
name &&
autoplay &&
acceleratedValues.has(name) &&
(name !== "transform" || !transformTemplate) &&
/**
* If we're outputting values to onUpdate then we can't use WAAPI as there's
* no way to read the value from WAAPI every frame.
*/
!onUpdate &&
!repeatDelay &&
repeatType !== "mirror" &&
damping !== 0 &&
type !== "inertia"
)
}
Can anybody please confirm this? @JeroenReumkens This does not fix the element, not being opacity absolute 0.
Also please be gentle, I have never contributed anything to motion before. Thanks!
I'm running into this same problem where I have elements that I want to both animate in on mount, and animate on scroll.
const { scrollY } = useScroll()
// in effect:
animate(elements, { y: [-50, 0], opacity: [0, 1] }, // entry animation
{ delay: stagger(0.1, { from: "center", startDelay: 1 }) }
)
const anim = animate(elements, // scroll animation
{ y: [0, -400], opacity: [1, 0] },
{ delay: stagger(0.01, { from: "center" }), autoplay: false },
)
scrollY.on("change",
(y) => (h1ScrollAnim.time = remapRange(y, 0, document.body.scrollHeight, 0, 500) / 1000)
)
If I comment out the scroll callback, it still seems to run the animation. Which is odd because if I start scrolling, the animation correctly jumps back to the start and then scrubs as expected. It doesn't seem to matter if I .kill() or .cancel() the animations in the effect to clean them up or not. If I wrap the scroll animation inside the entry .then() callback, it still seems to run the animation.
The solution as of now is to create the scroll animation inside of the scroll callback if it doesn't exist.
Perhaps there is an easier way in motion/react to have animations with sequences/timeline tied to scroll? I'm still learning this awesome library.