diff --git a/packages/framer-motion/src/animation/animate/index.ts b/packages/framer-motion/src/animation/animate/index.ts index 05cf0ec6d9..8fecca2ef4 100644 --- a/packages/framer-motion/src/animation/animate/index.ts +++ b/packages/framer-motion/src/animation/animate/index.ts @@ -22,11 +22,17 @@ function isSequence(value: unknown): value is AnimationSequence { return Array.isArray(value) && value.some(Array.isArray) } +interface ScopedAnimateOptions { + scope?: AnimationScope + reduceMotion?: boolean +} + /** * Creates an animation function that is optionally scoped * to a specific element. */ -export function createScopedAnimate(scope?: AnimationScope) { +export function createScopedAnimate(options: ScopedAnimateOptions = {}) { + const { scope, reduceMotion } = options /** * Animate a sequence */ @@ -106,7 +112,9 @@ export function createScopedAnimate(scope?: AnimationScope) { if (isSequence(subjectOrSequence)) { animations = animateSequence( subjectOrSequence, - optionsOrKeyframes as SequenceOptions, + reduceMotion !== undefined + ? { reduceMotion, ...(optionsOrKeyframes as SequenceOptions) } + : (optionsOrKeyframes as SequenceOptions), scope ) } else { @@ -118,7 +126,9 @@ export function createScopedAnimate(scope?: AnimationScope) { animations = animateSubject( subjectOrSequence as ElementOrSelector, optionsOrKeyframes as DOMKeyframesDefinition, - rest as DynamicAnimationOptions, + (reduceMotion !== undefined + ? { reduceMotion, ...rest } + : rest) as DynamicAnimationOptions, scope ) } diff --git a/packages/framer-motion/src/animation/hooks/use-animate.ts b/packages/framer-motion/src/animation/hooks/use-animate.ts index deb996cb22..e7b76d28dd 100644 --- a/packages/framer-motion/src/animation/hooks/use-animate.ts +++ b/packages/framer-motion/src/animation/hooks/use-animate.ts @@ -1,8 +1,10 @@ "use client" +import { useMemo } from "react" import { AnimationScope } from "motion-dom" import { useConstant } from "../../utils/use-constant" import { useUnmountEffect } from "../../utils/use-unmount-effect" +import { useReducedMotionConfig } from "../../utils/reduced-motion/use-reduced-motion-config" import { createScopedAnimate } from "../animate" export function useAnimate() { @@ -11,7 +13,12 @@ export function useAnimate() { animations: [], })) - const animate = useConstant(() => createScopedAnimate(scope)) + const reduceMotion = useReducedMotionConfig() ?? undefined + + const animate = useMemo( + () => createScopedAnimate({ scope, reduceMotion }), + [scope, reduceMotion] + ) useUnmountEffect(() => { scope.animations.forEach((animation) => animation.stop()) diff --git a/packages/framer-motion/src/animation/sequence/types.ts b/packages/framer-motion/src/animation/sequence/types.ts index 3a9cb804c7..2797db4e4d 100644 --- a/packages/framer-motion/src/animation/sequence/types.ts +++ b/packages/framer-motion/src/animation/sequence/types.ts @@ -74,6 +74,7 @@ export interface SequenceOptions extends AnimationPlaybackOptions { delay?: number duration?: number defaultTransition?: Transition + reduceMotion?: boolean } export interface AbsoluteKeyframe { diff --git a/packages/motion-dom/src/animation/interfaces/visual-element-target.ts b/packages/motion-dom/src/animation/interfaces/visual-element-target.ts index 62db8a3cdb..d69a647514 100644 --- a/packages/motion-dom/src/animation/interfaces/visual-element-target.ts +++ b/packages/motion-dom/src/animation/interfaces/visual-element-target.ts @@ -39,6 +39,8 @@ export function animateTarget( ...target } = targetAndTransition + const reduceMotion = (transition as { reduceMotion?: boolean })?.reduceMotion + if (transitionOverride) transition = transitionOverride const animations: AnimationPlaybackControlsWithThen[] = [] @@ -106,12 +108,15 @@ export function animateTarget( addValueToWillChange(visualElement, key) + const shouldReduceMotion = + reduceMotion ?? visualElement.shouldReduceMotion + value.start( animateMotionValue( key, value, valueTarget, - visualElement.shouldReduceMotion && positionalKeys.has(key) + shouldReduceMotion && positionalKeys.has(key) ? { type: false } : valueTransition, visualElement, diff --git a/packages/motion-dom/src/animation/types.ts b/packages/motion-dom/src/animation/types.ts index 1c62cb8ac4..e32b2ad066 100644 --- a/packages/motion-dom/src/animation/types.ts +++ b/packages/motion-dom/src/animation/types.ts @@ -593,9 +593,21 @@ export type ValueAnimationWithDynamicDelay = Omit< delay?: number | DynamicOption } +interface ReduceMotionOption { + /** + * Whether to reduce motion for transform/layout animations. + * + * - `true`: Skip transform/layout animations (instant transition) + * - `false`: Always animate transforms/layout + * - `undefined`: Use device preference (default behavior) + */ + reduceMotion?: boolean +} + export type AnimationOptions = - | ValueAnimationWithDynamicDelay + | (ValueAnimationWithDynamicDelay & ReduceMotionOption) | (ValueAnimationWithDynamicDelay & + ReduceMotionOption & StyleTransitions & SVGPathTransitions & SVGForcedAttrTransitions &