From 3ca1d003f2a8329a8ea12b366bd3a6eb5b114627 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 5 Aug 2026 06:46:51 +0200 Subject: [PATCH 1/3] Fix #3779: keep SVG opacity rendering in CSS Align JS and WAAPI SVG opacity updates so instant transitions cannot leave a stale inline style. Co-authored-by: Cursor --- .../src/tests/waapi-svg-zero-duration.tsx | 32 +++++++++++++++++++ .../integration/waapi-svg-zero-duration.ts | 19 +++++++++++ .../cypress/integration/waapi-svg.ts | 9 +++--- .../src/render/svg/utils/build-attrs.ts | 7 ++-- 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 dev/react/src/tests/waapi-svg-zero-duration.tsx create mode 100644 packages/framer-motion/cypress/integration/waapi-svg-zero-duration.ts diff --git a/dev/react/src/tests/waapi-svg-zero-duration.tsx b/dev/react/src/tests/waapi-svg-zero-duration.tsx new file mode 100644 index 0000000000..ed13105416 --- /dev/null +++ b/dev/react/src/tests/waapi-svg-zero-duration.tsx @@ -0,0 +1,32 @@ +import { motion } from "framer-motion" +import { useState } from "react" + +export const App = () => { + const [hidden, setHidden] = useState(false) + + return ( + <> + + +
chip
+ + + + ) +} diff --git a/packages/framer-motion/cypress/integration/waapi-svg-zero-duration.ts b/packages/framer-motion/cypress/integration/waapi-svg-zero-duration.ts new file mode 100644 index 0000000000..1f0d3ecb23 --- /dev/null +++ b/packages/framer-motion/cypress/integration/waapi-svg-zero-duration.ts @@ -0,0 +1,19 @@ +describe("waapi-svg-zero-duration", () => { + it("Restores SVG opacity with a zero-duration animation", () => { + cy.visit("?test=waapi-svg-zero-duration") + .get("#toggle") + .click() + .wait(400) + .get("#chip") + .then(([$chip]: any) => { + expect(getComputedStyle($chip).opacity).to.equal("0") + }) + .get("#toggle") + .click() + .wait(50) + .get("#chip") + .then(([$chip]: any) => { + expect(getComputedStyle($chip).opacity).to.equal("1") + }) + }) +}) diff --git a/packages/framer-motion/cypress/integration/waapi-svg.ts b/packages/framer-motion/cypress/integration/waapi-svg.ts index 4789a3a7be..114e80b23b 100644 --- a/packages/framer-motion/cypress/integration/waapi-svg.ts +++ b/packages/framer-motion/cypress/integration/waapi-svg.ts @@ -46,7 +46,7 @@ describe("waapi-svg", () => { expect(translateX).to.be.greaterThan(3) // Per-frame rendered values remain at the initial keyframe - expect($circle.getAttribute("opacity")).to.equal("1") + expect($circle.style.opacity).to.equal("1") expect($circle.style.transform).to.equal("translateX(0px)") }) cy.get("#rect").then(([$rect]: any) => { @@ -58,7 +58,7 @@ describe("waapi-svg", () => { const [scaleX] = parseMatrix(computed.transform) expect(scaleX).to.be.greaterThan(1.01) - expect($rect.getAttribute("opacity")).to.equal("1") + expect($rect.style.opacity).to.equal("1") expect($rect.style.transform).to.equal("scale(1)") }) }) @@ -74,8 +74,9 @@ describe("waapi-svg", () => { .getAnimations() .flatMap((animation) => Object.keys( - (animation.effect as KeyframeEffect).getKeyframes()[0] ?? - {} + ( + animation.effect as KeyframeEffect + ).getKeyframes()[0] ?? {} ) ) diff --git a/packages/motion-dom/src/render/svg/utils/build-attrs.ts b/packages/motion-dom/src/render/svg/utils/build-attrs.ts index 95e4a48aab..c0b8175ee3 100644 --- a/packages/motion-dom/src/render/svg/utils/build-attrs.ts +++ b/packages/motion-dom/src/render/svg/utils/build-attrs.ts @@ -51,13 +51,16 @@ export function buildSVGAttrs( const { attrs, style } = state /** - * However, we apply transforms as CSS transforms. - * So if we detect a transform, transformOrigin we take it from attrs and copy it into style. + * However, we apply transforms and opacity as CSS styles. */ if (attrs.transform) { style.transform = attrs.transform delete attrs.transform } + if (attrs.opacity !== undefined) { + style.opacity = attrs.opacity + delete attrs.opacity + } if (style.transform || attrs.transformOrigin) { style.transformOrigin = attrs.transformOrigin ?? "50% 50%" delete attrs.transformOrigin From 233593b50151a414101d37d93762824f3db15784 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 5 Aug 2026 06:57:05 +0200 Subject: [PATCH 2/3] Fix #3779: consolidate SVG style properties Move transform, opacity, and motion-path properties through one shared style-routing loop. Co-authored-by: Cursor --- .../src/render/svg/utils/build-attrs.ts | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/packages/motion-dom/src/render/svg/utils/build-attrs.ts b/packages/motion-dom/src/render/svg/utils/build-attrs.ts index c0b8175ee3..30c3e920c0 100644 --- a/packages/motion-dom/src/render/svg/utils/build-attrs.ts +++ b/packages/motion-dom/src/render/svg/utils/build-attrs.ts @@ -4,10 +4,9 @@ import { ResolvedValues } from "../../types" import { SVGRenderState } from "../types" import { buildSVGPath } from "./path" -/** - * CSS Motion Path properties that should remain as CSS styles on SVG elements. - */ -const cssMotionPathProperties = [ +const cssStyleProperties = [ + "transform", + "opacity", "offsetDistance", "offsetPath", "offsetRotate", @@ -50,17 +49,13 @@ export function buildSVGAttrs( state.style = {} const { attrs, style } = state - /** - * However, we apply transforms and opacity as CSS styles. - */ - if (attrs.transform) { - style.transform = attrs.transform - delete attrs.transform - } - if (attrs.opacity !== undefined) { - style.opacity = attrs.opacity - delete attrs.opacity + for (const key of cssStyleProperties) { + if (attrs[key] !== undefined) { + style[key] = attrs[key] + delete attrs[key] + } } + if (style.transform || attrs.transformOrigin) { style.transformOrigin = attrs.transformOrigin ?? "50% 50%" delete attrs.transformOrigin @@ -75,13 +70,6 @@ export function buildSVGAttrs( delete attrs.transformBox } - for (const key of cssMotionPathProperties) { - if (attrs[key] !== undefined) { - style[key] = attrs[key] - delete attrs[key] - } - } - // Render attrX/attrY/attrScale as attributes if (attrX !== undefined) attrs.x = attrX if (attrY !== undefined) attrs.y = attrY From 165541a9c0b8bb76d459825a8bf5a738ab04f224 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Wed, 5 Aug 2026 11:06:40 +0200 Subject: [PATCH 3/3] Fix #3779: read SVG CSS properties from styles Read shared SVG style properties from computed CSS with an attribute fallback, and cover svgEffect plus instant transform restoration. Co-authored-by: Cursor --- .../src/tests/waapi-svg-zero-duration.tsx | 18 ++++++ .../integration/waapi-svg-zero-duration.ts | 18 ++++++ .../src/effects/__tests__/svg-effect.test.ts | 29 +++++++++ .../src/render/svg/SVGVisualElement.ts | 9 ++- .../svg/__tests__/SVGVisualElement.test.ts | 63 +++++++++++++++++++ .../src/render/svg/utils/build-attrs.ts | 2 +- 6 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 packages/motion-dom/src/render/svg/__tests__/SVGVisualElement.test.ts diff --git a/dev/react/src/tests/waapi-svg-zero-duration.tsx b/dev/react/src/tests/waapi-svg-zero-duration.tsx index ed13105416..b09ba56019 100644 --- a/dev/react/src/tests/waapi-svg-zero-duration.tsx +++ b/dev/react/src/tests/waapi-svg-zero-duration.tsx @@ -26,6 +26,24 @@ export const App = () => { >
chip
+