Skip to content

Commit 0fb160f

Browse files
committed
fix: useAnimate respects MotionConfig.skipAnimations
The imperative useAnimate hook only checked reducedMotion via useReducedMotionConfig(), ignoring MotionConfig's skipAnimations prop. This meant WAAPI animations were still created even when skipAnimations was true, just with reduced timing. This is problematic for e2e testing tools like Playwright, which call element.getAnimations() for stability checks. WebKit reports these zero-duration WAAPI animations as running, causing timeouts. When skipAnimations is true, the returned animate function now resolves immediately via an empty GroupAnimationWithThen without creating any WAAPI animations, consistent with how declarative animations behave when skipAnimations is set on MotionConfig. Fixes #3679
1 parent e842024 commit 0fb160f

2 files changed

Lines changed: 58 additions & 4 deletions

File tree

packages/framer-motion/src/animation/hooks/__tests__/use-animate.test.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import "@testing-library/jest-dom"
22
import { render } from "@testing-library/react"
33
import { useEffect } from "react"
4+
import { MotionConfig } from "../../../components/MotionConfig"
45
import { useAnimate } from "../use-animate"
56

67
describe("useAnimate", () => {
@@ -115,4 +116,39 @@ describe("useAnimate", () => {
115116

116117
expect(frameCount).toEqual(3)
117118
})
119+
120+
test("Skips animations when MotionConfig skipAnimations is true", () => {
121+
return new Promise<void>((resolve) => {
122+
const Component = () => {
123+
const [scope, animate] = useAnimate()
124+
125+
useEffect(() => {
126+
const animation = animate(
127+
scope.current,
128+
{ opacity: 0.5 },
129+
{ duration: 1 }
130+
)
131+
132+
// Animation should not be tracked in scope
133+
expect(scope.animations.length).toBe(0)
134+
135+
animation.then(() => {
136+
// Element style should not be changed
137+
expect(scope.current).not.toHaveStyle(
138+
"opacity: 0.5;"
139+
)
140+
resolve()
141+
})
142+
})
143+
144+
return <div ref={scope} />
145+
}
146+
147+
render(
148+
<MotionConfig skipAnimations>
149+
<Component />
150+
</MotionConfig>
151+
)
152+
})
153+
})
118154
})
Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"use client"
22

3-
import { useMemo } from "react"
4-
import { AnimationScope } from "motion-dom"
3+
import { useContext, useMemo } from "react"
4+
import { AnimationScope, GroupAnimationWithThen } from "motion-dom"
55
import { useConstant } from "../../utils/use-constant"
66
import { useUnmountEffect } from "../../utils/use-unmount-effect"
77
import { useReducedMotionConfig } from "../../utils/reduced-motion/use-reduced-motion-config"
8+
import { MotionConfigContext } from "../../context/MotionConfigContext"
89
import { createScopedAnimate } from "../animate"
910

1011
export function useAnimate<T extends Element = any>() {
@@ -13,11 +14,15 @@ export function useAnimate<T extends Element = any>() {
1314
animations: [],
1415
}))
1516

17+
const { skipAnimations } = useContext(MotionConfigContext)
1618
const reduceMotion = useReducedMotionConfig() ?? undefined
1719

1820
const animate = useMemo(
19-
() => createScopedAnimate({ scope, reduceMotion }),
20-
[scope, reduceMotion]
21+
() =>
22+
skipAnimations
23+
? createNoopAnimate(scope)
24+
: createScopedAnimate({ scope, reduceMotion }),
25+
[scope, reduceMotion, skipAnimations]
2126
)
2227

2328
useUnmountEffect(() => {
@@ -27,3 +32,16 @@ export function useAnimate<T extends Element = any>() {
2732

2833
return [scope, animate] as [AnimationScope<T>, typeof animate]
2934
}
35+
36+
/**
37+
* When skipAnimations is true, return an animate function that resolves
38+
* immediately without creating any WAAPI animations. This prevents
39+
* browsers (particularly WebKit) from reporting running animations via
40+
* element.getAnimations(), which can break tools like Playwright that
41+
* check element stability.
42+
*/
43+
function createNoopAnimate<T extends Element>(scope: AnimationScope<T>) {
44+
return ((..._args: any[]) => {
45+
return new GroupAnimationWithThen([])
46+
}) as ReturnType<typeof createScopedAnimate>
47+
}

0 commit comments

Comments
 (0)