Skip to content

Fix MotionValues on SVG transform attribute rendering as [object Object]#3629

Merged
mattgperry merged 1 commit intomainfrom
worktree-fix-issue-3082
Mar 12, 2026
Merged

Fix MotionValues on SVG transform attribute rendering as [object Object]#3629
mattgperry merged 1 commit intomainfrom
worktree-fix-issue-3082

Conversation

@mattgperry
Copy link
Collaborator

Summary

  • Bug: Passing a MotionValue to <motion.g transform={motionValue}> rendered [object Object] as the DOM attribute instead of the resolved value
  • Cause: filterProps forwarded raw MotionValue objects to the DOM. For most SVG attributes (e.g. cx, fill), visualProps from useSVGProps overrides this, but transform is special — buildSVGAttrs moves it from attrs to style (CSS transform), so there was no override
  • Fix: Filter out MotionValue objects in filterProps so they're never passed to the DOM. The motion value system already handles them through the visual element's value map

Also fixes a pre-existing TS error in VisualElementDragControls that blocked the test suite.

Fixes #3082

Test plan

  • Added unit test proving MotionValue on <motion.g transform={...}> renders correctly (not [object Object])
  • All 92 test suites pass (767 tests)
  • Build succeeds

🤖 Generated with Claude Code

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-apps
Copy link

greptile-apps bot commented Mar 11, 2026

Greptile Summary

This PR fixes a bug where passing a MotionValue to <motion.g transform={motionValue}> would render [object Object] as the DOM attribute instead of the resolved value. The fix adds an isMotionValue guard in filterProps so raw MotionValue objects are never forwarded to the DOM — the motion value subscription system already handles applying the actual value. A TypeScript workaround in VisualElementDragControls is also included to unblock the test suite.

Key changes:

  • filter-props.ts: Adds a one-line isMotionValue check before the shouldForward logic, cleanly preventing any MotionValue object from leaking into DOM attributes for all prop names, not just transform
  • component-svg.test.tsx: Adds a regression test that verifies transform is not rendered as [object Object] and is correctly applied as a CSS style
  • VisualElementDragControls.ts: Works around a pre-existing TypeScript error where dragSnapToOrigin (typed boolean | "x" | "y") was compared against the axis string using as unknown to silence the compiler

Confidence Score: 4/5

  • Safe to merge — the core fix is minimal, well-targeted, and backed by a passing test suite
  • The main change is a single guard line that correctly filters out MotionValue objects before they can reach the DOM. The fix is additive and does not alter the existing flow for non-MotionValue props. Minor style concerns exist in the test assertion and the TypeScript cast, but neither affects correctness.
  • No files require special attention, though VisualElementDragControls.ts has a TypeScript cast that should ideally be resolved at the type-definition level in a follow-up.

Important Files Changed

Filename Overview
packages/framer-motion/src/render/dom/utils/filter-props.ts Adds an isMotionValue guard before the shouldForward check, ensuring MotionValue objects are never leaked to the DOM as raw objects. The placement is correct and the logic is sound — the motion value subscription system already owns the actual DOM update pathway.
packages/framer-motion/src/motion/tests/component-svg.test.tsx Adds a regression test for the MotionValue-on-transform bug. The CSS-style assertion is solid, but the not.toBe("[object Object]") assertion is a weak negative check that passes trivially when the attribute is absent (null). A toBeNull() assertion would be more precise and informative.
packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts Pre-existing TypeScript error fixed via as unknown cast to unblock the test suite. Functionally correct at runtime since dragSnapToOrigin does accept `"x"

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
Loading

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]")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
(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.

@mattgperry mattgperry merged commit 25bf593 into main Mar 12, 2026
8 of 9 checks passed
@mattgperry mattgperry deleted the worktree-fix-issue-3082 branch March 12, 2026 05:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] MotionValues cannot be used in motion.g element's transform attribute

1 participant