diff --git a/dev/react/src/tests/layout-motion-value.tsx b/dev/react/src/tests/layout-motion-value.tsx new file mode 100644 index 0000000000..9b8b98f906 --- /dev/null +++ b/dev/react/src/tests/layout-motion-value.tsx @@ -0,0 +1,30 @@ +import { motion, useMotionValue } from "framer-motion" + +export const App = () => { + const width = useMotionValue(100) + + return ( + <> + 0.5 }} + /> + + + ) +} diff --git a/packages/framer-motion/cypress/integration/layout-motion-value.ts b/packages/framer-motion/cypress/integration/layout-motion-value.ts new file mode 100644 index 0000000000..67707dfdfa --- /dev/null +++ b/packages/framer-motion/cypress/integration/layout-motion-value.ts @@ -0,0 +1,21 @@ +describe("Layout animation with MotionValue", () => { + it("Triggers layout animation when MotionValue changes a layout-affecting property", () => { + cy.visit("?test=layout-motion-value") + .wait(50) + .get("#box") + .should(([$box]: any) => { + const bbox = $box.getBoundingClientRect() + expect(bbox.width).to.equal(100) + }) + .get("#toggle") + .trigger("click") + .wait(50) + .get("#box") + .should(([$box]: any) => { + const bbox = $box.getBoundingClientRect() + // With ease: () => 0.5, layout animation freezes at 50% + // Width should be 200 (midpoint of 100→300), not 300 (no animation) + expect(bbox.width).to.equal(200) + }) + }) +}) diff --git a/packages/motion-dom/src/render/VisualElement.ts b/packages/motion-dom/src/render/VisualElement.ts index 8a8075891e..40787e6368 100644 --- a/packages/motion-dom/src/render/VisualElement.ts +++ b/packages/motion-dom/src/render/VisualElement.ts @@ -45,6 +45,15 @@ import { } from "./utils/reduced-motion" import { resolveVariantFromProps } from "./utils/resolve-variants" +const layoutKeys = new Set([ + "width", + "height", + "top", + "left", + "right", + "bottom", +]) + const propEventHandlers = [ "AnimationStart", "AnimationComplete", @@ -567,6 +576,7 @@ export abstract class VisualElement< } const valueIsTransform = transformProps.has(key) + const valueIsLayout = !valueIsTransform && layoutKeys.has(key) if (valueIsTransform && this.onBindTransform) { this.onBindTransform() @@ -579,8 +589,15 @@ export abstract class VisualElement< this.props.onUpdate && frame.preRender(this.notifyUpdate) - if (valueIsTransform && this.projection) { - this.projection.isTransformDirty = true + if (this.projection) { + if (valueIsTransform) { + this.projection.isTransformDirty = true + } else if (valueIsLayout) { + this.projection.willUpdate() + frame.postRender( + () => this.projection?.root?.didUpdate() + ) + } } this.scheduleRender()