Skip to content

Fix the SVG transform-origin during initial mount#3154

Merged
mattgperry merged 16 commits intomotiondivision:mainfrom
Taeyeon-Lim:fix/svg-transform-origin
Apr 24, 2025
Merged

Fix the SVG transform-origin during initial mount#3154
mattgperry merged 16 commits intomotiondivision:mainfrom
Taeyeon-Lim:fix/svg-transform-origin

Conversation

@Taeyeon-Lim
Copy link
Contributor

Dimension is measured on the client side. Therefore, the origin cannot be measured on the first mount, resulting in a jump. Before the dimension is measured, set transformBox to "fill-box" and center the origin to leave the initial origin to the browser.

This is only valid when manually calculating the last value of transformOrigin.

ref: #2949

…2949)

Dimension is measured on the client side. Therefore, the origin cannot be measured on the first mount, resulting in a jump. Before the dimension is measured, set "transformBox" to "Fill Box" and center the origin to leave the initial origin to the browser.

This behavior is only valid when manually calculating the last value of "transformOrigin".
@mattgperry
Copy link
Collaborator

I'm a little concerned about the extra bundle size here, but I have to ask, if this works, is it possible for us to just ditch measuring the SVG elements entirely and use this fill-box instead?

@Taeyeon-Lim
Copy link
Contributor Author

Unfortunately, I don't know exactly what context "calcSVGTransformOrigin" was added in, so it's hard to give you a definite answer. "fill-box" is widely used, but I think I can keep "calcSVGTransformOrigin" if it requires a manual accurate calculation.

As a temporary measure, I took the form of leaving the server to the browser. I need to make sure most browsers guarantee the same behavior of the "fill-box" and "transform-origin: 50% 50%" combination. What I've checked are firefox and chrome browsers.

For now, I think it's possible. However, I think someone might want to use "view-box". I think we might consider whether to give this option to the user or not.

@mattgperry
Copy link
Collaborator

Ah ok - well currently as it stands the intended behaviour of all this measurement is to force transforms to apply to SVG elements the same way they do HTML elements. There's no option to have the default behaviour. So by setting originX: 0.1 we want an origin point thats 10% into the width of the SVG element itself, rather than the parent view box. In other words, if this behaviour is attainable by setting transform-box: fill-box we could replace all that logic with just that one style in useSVGProps

@Taeyeon-Lim
Copy link
Contributor Author

Maybe, I think it's possible.

@mattgperry
Copy link
Collaborator

mattgperry commented Apr 15, 2025

Having a quick play, this is what we're trying to do with all the measurements. This would be a big win for performance and filesize. Is this something you'd be interested in taking a look at or would you prefer me to give it a go?

@Taeyeon-Lim
Copy link
Contributor Author

I'll try it.

@Taeyeon-Lim
Copy link
Contributor Author

Hi, I have a question. The origin doesn't apply to HTML elements (not SVG). Wouldn't it be better if origin could apply to HTML elements as well?...or is it not necessary?

example

// HTML element origin
<motion.div style={{ originX : 0.1 }}  /> 
  • current : "50% 50%"
  • better(?) : "10% 50%"

@mattgperry
Copy link
Collaborator

@Taeyeon-Lim It should be the case that originX/Y already have this effect on transform-origin (see buildStyles)

@Taeyeon-Lim
Copy link
Contributor Author

oh..., I understand. Thank you.

Change the existing origin calculation from manual to automatic. To do this, switch the transform-box style from the default value to 'fill-box'.
const staticMarkup = renderToStaticMarkup(<Component />)
const string = renderToString(<Component />)

const expectedMarkup = `<article><main draggable="false" style="z-index:unset;transform:none;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;touch-action:pan-x"></main></article>`
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should only be the case if originX etc or transformOrigin have been explicitly set.

)

expect(div).toBe(
'<div style="transform:translateX(100px) translateY(200px)"></div>'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Likewise we definitely don't want to set this if it wasn't explicitly set by the user - unless this is an SVG with a transform

)

expect(rect).toBe(
'<rect mask="" class="test" style="background:#fff"></rect>'
Copy link
Collaborator

Choose a reason for hiding this comment

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

How come this one doesn't have transform-box:fill-box?

* undefined origins.
*/
if (hasTransformOrigin) {
if (hasTransformOrigin || style.transform) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you move this logic to the svg/utils/build-styles file this will remove the style from HTML elements

useVisualState: makeUseVisualState({
scrapeMotionValuesFromProps: scrapeSVGProps,
createRenderState: createSvgRenderState,
onUpdate: ({
Copy link
Collaborator

Choose a reason for hiding this comment

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

🥹

@mattgperry
Copy link
Collaborator

I left some comments, but this looks amazing. I think if we can get transform-origin from appearing in HTML elements then we're set.

@Taeyeon-Lim
Copy link
Contributor Author

I'm sorry for the late response and thank you for the comment. Well, I got the default behavior wrong. What you said meant it only changed when you were given transform-origin or transform by the user...
I will remove the style.transform of html/utils/build-styles and change the transform-origin of SVG to operate when there is only transform-origin from html. If I do this, unlike HTML, if there is only transform, transform-origin will act like 0, 0 instead of 50% 50%, right? Or, should I include this case as well?

@mattgperry
Copy link
Collaborator

mattgperry commented Apr 17, 2025 via email

@Taeyeon-Lim
Copy link
Contributor Author

I've understood your intentions clearly, and I'll take my time to ensure it's done well. don't worry

mattgperry and others added 6 commits April 19, 2025 00:51
Modify the behavior of "transform-origin, -box" in SVG to apply user-provided values correctly.

Changes include:
- Default "transform" value set to "50% 50%" when "transform-origin" is not provided.
- "transform-origin" applies the input value as is.
- "transform" or "transform-origin" triggers the application of "transform-box: fill-box".
This type is no longer in use.
@mattgperry
Copy link
Collaborator

Thanks for your work on this @Taeyeon-Lim! There's just the conflicts to resolve and we can publish this - I'd do it myself but I'm getting a message that they're too complex to resolve in the web editor.

@Taeyeon-Lim Taeyeon-Lim force-pushed the fix/svg-transform-origin branch from bc2897c to 09c94a6 Compare April 23, 2025 14:48
@Taeyeon-Lim
Copy link
Contributor Author

Taeyeon-Lim commented Apr 23, 2025

I see, I'll try to solve it.

@Taeyeon-Lim Taeyeon-Lim force-pushed the fix/svg-transform-origin branch from 09c94a6 to b5586d0 Compare April 23, 2025 15:18
@Taeyeon-Lim
Copy link
Contributor Author

The conflict is resolved, but the content has gotten a little messy. Is it better to do a new PR?

@mattgperry
Copy link
Collaborator

@Taeyeon-Lim No it's perfect, thanks for all your work on this!

@mattgperry mattgperry merged commit 3b2b3e8 into motiondivision:main Apr 24, 2025
3 checks passed
@Taeyeon-Lim Taeyeon-Lim deleted the fix/svg-transform-origin branch April 25, 2025 06:09
mattgperry added a commit that referenced this pull request Mar 17, 2026
The bug (SVG transform-origin jumping on initial mount) was fixed in PR
#3154, which ensured transformBox: "fill-box" and transformOrigin: "50%
50%" are set before dimensions are measured. The fix was preserved during
the motion-dom refactoring. This commit adds targeted E2E tests to
prevent regression and closes the issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants