Describe the bug
Since framer-motion 13.3.0 (motion 13.3.0 and 13.4.0), animate() on an SVG element writes CSS custom properties as attributes (<path --x="0.68">) instead of setting them with style.setProperty. CSS var() never reads attributes, so anything driven by the variable stops animating. HTML elements are unaffected. Both the single-element and the sequence forms of animate() show it.
Real-world case: we trim an SVG path with stroke-dasharray: calc(var(--trim-end) - var(--trim-start)) 2 and animate the two variables with animate(). On 13.3.0 the trim no longer moves.
IMPORTANT: Provide a CodeSandbox reproduction of the bug
https://codesandbox.io/p/sandbox/d8crww
It runs the same animate() call against 13.2.0 and 13.4.0 side by side (via esm.sh with pinned deps). The 13.2.0 path trim animates; the 13.4.0 one stays a full line, and the readout shows --x landing in an attribute instead of style.
Steps to reproduce
import { animate, frame } from "motion"
document.body.innerHTML = `
<svg width="100" height="100"><path id="p" d="M0 0L100 100" stroke="black"/></svg>
<div id="d"></div>`
const path = document.getElementById("p")
const div = document.getElementById("d")
for (const el of [path, div]) {
const a = animate(el, { "--x": [0, 1] }, { duration: 1, autoplay: false })
a.time = 0.5
}
const tick = () => new Promise((r) => frame.postRender(r))
await tick()
await tick()
console.log(path.outerHTML)
console.log(JSON.stringify({
svg: path.style.getPropertyValue("--x"),
html: div.style.getPropertyValue("--x"),
}))
The sequence form animate([[path, { "--y": [0, 1] }, { duration: 1 }]]) gives the same result.
Expected behavior
--x lands in inline style on the SVG path, as it does on the div. This is what 13.2.0 prints:
<path id="p" d="M0 0L100 100" stroke="black" style="--x: 0.6846233151445631;"></path>
{"svg":"0.6846233151445631","html":"0.6846233151445631"}
Actual output on 13.3.0 and 13.4.0:
<path id="p" d="M0 0L100 100" stroke="black" --x="0.6846233151445631"></path>
{"svg":"","html":"0.6846233151445631"}
Note for reproducing the working case: npm i motion@13.2.0 alone still installs framer-motion 13.4.0 via ^13.2.0. Add an override pinning framer-motion to 13.2.0. framer-motion 13.2.0 with motion-dom 13.3.0 works, so the regression comes in with framer-motion 13.3.0.
Environment details
Chromium via Playwright 1.63, macOS. motion 13.2.0 / 13.3.0 / 13.4.0.
Cause
Suggested fix
Route CSS variables to the style writer in addSVGValue, e.g. isCSSVar(key) || key in element.style ? addStyleValue : addAttrValue, and add an SVG case next to the existing CSS-variable test.
Let me know if you'd like me to submit a PR with this approach.
Related change, possibly intended
Animating fill on an SVG path now writes inline style and leaves the fill attribute alone, where 13.2.0 animated the attribute. This changes rendering when author CSS also sets fill: with a stylesheet rule fill: green, 13.2.0's animation lost to the stylesheet, and 13.3.0's inline style wins. Mentioning it in case it wasn't deliberate (see also #3779).
Describe the bug
Since framer-motion 13.3.0 (motion 13.3.0 and 13.4.0),
animate()on an SVG element writes CSS custom properties as attributes (<path --x="0.68">) instead of setting them withstyle.setProperty. CSSvar()never reads attributes, so anything driven by the variable stops animating. HTML elements are unaffected. Both the single-element and the sequence forms ofanimate()show it.Real-world case: we trim an SVG path with
stroke-dasharray: calc(var(--trim-end) - var(--trim-start)) 2and animate the two variables withanimate(). On 13.3.0 the trim no longer moves.IMPORTANT: Provide a CodeSandbox reproduction of the bug
https://codesandbox.io/p/sandbox/d8crww
It runs the same
animate()call against 13.2.0 and 13.4.0 side by side (via esm.sh with pinned deps). The 13.2.0 path trim animates; the 13.4.0 one stays a full line, and the readout shows--xlanding in an attribute instead of style.Steps to reproduce
The sequence form
animate([[path, { "--y": [0, 1] }, { duration: 1 }]])gives the same result.Expected behavior
--xlands in inline style on the SVG path, as it does on the div. This is what 13.2.0 prints:Actual output on 13.3.0 and 13.4.0:
Note for reproducing the working case:
npm i motion@13.2.0alone still installs framer-motion 13.4.0 via^13.2.0. Add an override pinningframer-motionto 13.2.0. framer-motion 13.2.0 with motion-dom 13.3.0 works, so the regression comes in with framer-motion 13.3.0.Environment details
Chromium via Playwright 1.63, macOS. motion 13.2.0 / 13.3.0 / 13.4.0.
Cause
animate()to stop creating a VisualElement for Element subjects and call motion-dom'sanimateElementinstead.addSVGValueinpackages/motion-dom/src/effects/svg/index.tspicks the writer withkey in element.style ? addStyleValue : addAttrValue. A--*key is neverin element.style, so CSS variables go toaddAttrValue. That check predates 13.3.0;animate()only started reaching it with Drive animate() DOM elements through styleEffect instead of a VisualElement #3815.element.test.tscovers CSS variables on HTML ("renders transforms, styles and CSS variables via styleEffect") but has no SVG equivalent.Suggested fix
Route CSS variables to the style writer in
addSVGValue, e.g.isCSSVar(key) || key in element.style ? addStyleValue : addAttrValue, and add an SVG case next to the existing CSS-variable test.Let me know if you'd like me to submit a PR with this approach.
Related change, possibly intended
Animating
fillon an SVG path now writes inline style and leaves thefillattribute alone, where 13.2.0 animated the attribute. This changes rendering when author CSS also setsfill: with a stylesheet rulefill: green, 13.2.0's animation lost to the stylesheet, and 13.3.0's inline style wins. Mentioning it in case it wasn't deliberate (see also #3779).