Fix MotionValues on SVG transform attribute rendering as [object Object]#3629
Fix MotionValues on SVG transform attribute rendering as [object Object]#3629mattgperry merged 1 commit intomainfrom
Conversation
When a MotionValue was passed as the transform prop on an SVG element like
<motion.g transform={motionValue}>, the raw MotionValue object was passed
through filterProps to the DOM, where React called toString() on it,
producing "[object Object]".
The fix filters out MotionValue objects in filterProps so they are never
forwarded as DOM attributes. The motion value system already handles these
values through the visual element's value map and render state.
Also fixes a pre-existing TS error in VisualElementDragControls where
dragSnapToOrigin (typed boolean | "x" | "y") was compared to a string axis.
Fixes #3082
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR fixes a bug where passing a Key changes:
Confidence Score: 4/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Prop passed to motion component\n(e.g. transform={motionValue})"] --> B{filterProps}
B --> C{key === 'values'\n&& typeof === 'object'?}
C -- yes --> SKIP1["skip (continue)"]
C -- no --> D{"isMotionValue(prop[key])?"}
D -- yes --> SKIP2["skip (continue)\n🆕 NEW CHECK"]
D -- no --> E{shouldForward OR\nforwardMotionProps OR\n!isDom?}
E -- yes --> F["Add to filteredProps\n→ passed to DOM"]
E -- no --> SKIP3["skip (continue)"]
G["MotionValue subscription system"] --> H["visualElement value map"]
H --> I["useSVGProps / buildSVGAttrs\n→ CSS transform applied"]
SKIP2 -.->|"MotionValue handled separately"| G
Last reviewed commit: d79e0d4 |
|
|
||
| const gElement = container.querySelector("g")! | ||
| // The transform should NOT be rendered as "[object Object]" | ||
| expect(gElement.getAttribute("transform")).not.toBe("[object Object]") |
There was a problem hiding this comment.
Weak negative assertion
The assertion not.toBe("[object Object]") passes trivially if getAttribute("transform") returns null (which it will when the attribute is absent — the expected, correct behavior after filtering). Consider replacing it with a positive assertion that explicitly documents what the value should be:
| expect(gElement.getAttribute("transform")).not.toBe("[object Object]") | |
| expect(gElement.getAttribute("transform")).toBeNull() |
This would make the test self-documenting: after the fix, filterProps drops the MotionValue, so the transform DOM attribute should not be set at all (the value is applied via CSS transform instead).
| if ( | ||
| dragSnapToOrigin === true || | ||
| dragSnapToOrigin === axis | ||
| (dragSnapToOrigin as unknown) === axis |
There was a problem hiding this comment.
Over-broad TypeScript cast
Using as unknown fully erases type information and bypasses all TypeScript safety checks. The root cause is that getProps() returns MotionProps (from framer-motion's own types), but the type for dragSnapToOrigin in that interface appears to be narrower than the full boolean | "x" | "y" union defined in motion-dom/src/node/types.ts.
A more precise cast — or better yet, aligning MotionProps with the upstream type — would be safer:
| (dragSnapToOrigin as unknown) === axis | |
| (dragSnapToOrigin as boolean | "x" | "y") === axis |
If the mismatch is in MotionProps, the proper long-term fix is to ensure MotionProps.dragSnapToOrigin is typed as boolean | "x" | "y", which removes the need for a cast entirely.
Summary
<motion.g transform={motionValue}>rendered[object Object]as the DOM attribute instead of the resolved valuefilterPropsforwarded raw MotionValue objects to the DOM. For most SVG attributes (e.g.cx,fill),visualPropsfromuseSVGPropsoverrides this, buttransformis special —buildSVGAttrsmoves it from attrs to style (CSS transform), so there was no overridefilterPropsso they're never passed to the DOM. The motion value system already handles them through the visual element's value mapAlso fixes a pre-existing TS error in
VisualElementDragControlsthat blocked the test suite.Fixes #3082
Test plan
<motion.g transform={...}>renders correctly (not[object Object])🤖 Generated with Claude Code