Skip to content

Fix drag being blocked by variant transform string (#2807)#3728

Open
mattgperry wants to merge 1 commit into
mainfrom
fix/issue-2807-variant-transform-blocks-drag
Open

Fix drag being blocked by variant transform string (#2807)#3728
mattgperry wants to merge 1 commit into
mainfrom
fix/issue-2807-variant-transform-blocks-drag

Conversation

@mattgperry
Copy link
Copy Markdown
Collaborator

Summary

  • buildHTMLStyles previously preferred a literal transform string in latestValues over the computed transform from individual props. When a variant set transform: "..." and drag (or another gesture) was driving x/y motion values, the gesture's translations were silently dropped.
  • This change makes buildTransform win whenever x or y are present in latestValues, so a variant-set transform string no longer blocks drag.

Bug

Reported in #2807. Repro from the issue:

const variants: Variants = {
  none: (custom) => ({
    width: "fit-content",
    height: "fit-content",
    transform: custom || undefined,
  }),
}

<motion.div
  custom={positionBackup.current}
  initial="none"
  animate="none"
  variants={variants}
  drag
  dragControls={dragControls}
  dragListener={false}
/>

The none variant sets transform to a literal string. When drag updated x/y motion values, latestValues.transform was still truthy, so the old check (if (!latestValues.transform)) skipped buildTransform entirely and style.transform stayed at the variant string — drag did nothing.

Fix

packages/motion-dom/src/render/html/utils/build-styles.ts: extend the gate so we still run buildTransform when x or y is in latestValues. The previous behaviour is preserved when neither translate axis is set, so a useMotionTemplate-based transform string with non-translate motion values (e.g. scale) still wins — the existing "Prioritises transform over independent transforms" test continues to pass.

Test plan

  • New unit test in build-styles.test.ts asserts that { x: 50, transform: "translateX(100px)" } resolves to transform: "translateX(50px)". Fails without the fix.
  • New Cypress test drag-variant-transform.ts reproduces the issue's scenario (dragControls + dragListener={false} + variant transform string) and checks the element moves. Fails on main (left=150 — variant transform wins), passes with the fix.
  • Cypress passes on React 18 and React 19.
  • Full yarn test (798 tests) passes.

Fixes #2807

🤖 Generated with Claude Code

When a variant or style sets a literal `transform` string and drag (or
another gesture driving x/y motion values) is active, `buildHTMLStyles`
now prefers the result of `buildTransform` over the literal string so
that the gesture-driven values are reflected on the element.

Fixes #2807

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 13, 2026

Greptile Summary

This PR fixes a bug where a variant-supplied literal transform string in latestValues blocked buildTransform from running, causing drag (which drives x/y motion values) to silently have no visual effect. The fix adds a hasTranslate flag that allows buildTransform to win whenever x or y are present in latestValues, preserving the existing useMotionTemplate-wins-for-non-translate behaviour.

  • Core fix (build-styles.ts): extend the buildTransform gate so it fires when x or y are in latestValues, even if a literal transform string is also present.
  • Unit test (build-styles.test.ts): adds a new case asserting { x: 50, transform: "translateX(100px)" }transform: "translateX(50px)".
  • E2E test (drag-variant-transform.ts / drag-variant-transform.tsx): Cypress regression that reproduces the original dragControls + dragListener=false + variant transform scenario end-to-end.

Confidence Score: 4/5

Safe to merge — the fix is narrow and well-targeted, with both a unit test and a Cypress E2E regression guard.

The change is small and the logic is sound for the common x/y drag case. The only gap is that hasTranslate does not cover z, translateX, or translateY motion values — any element driven along those axes while a variant supplies a literal transform string would still silently lose its gesture translation. That gap is unlikely to affect real users today (drag drives x/y), but it leaves a latent inconsistency. All other existing tests are preserved.

packages/motion-dom/src/render/html/utils/build-styles.ts — the hasTranslate condition could be broadened to cover z, translateX, translateY, and translateZ for full consistency.

Important Files Changed

Filename Overview
packages/motion-dom/src/render/html/utils/build-styles.ts Core fix: extends the buildTransform gate to also fire when x or y are in latestValues, preventing a literal variant transform string from silently dropping drag translations. Logic is sound but hasTranslate only guards x and y, leaving translateX/translateY/z uncovered.
packages/framer-motion/src/render/html/utils/tests/build-styles.test.ts Adds a focused unit test confirming individual transform props win over a literal transform string when x is present. Test name slightly overstates scope (all props vs. only the x/y case), but coverage is correct for the bug.
packages/framer-motion/cypress/integration/drag-variant-transform.ts New Cypress E2E regression test that reproduces the exact user scenario: dragControls + dragListener=false + variant-set transform string. Correctly verifies initial position from variant transform and post-drag displacement.
dev/react/src/tests/drag-variant-transform.tsx Dev harness component for the Cypress test. Faithfully reproduces the original issue scenario with custom variant transform and dragControls.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[buildHTMLStyles called] --> B[Loop over latestValues]
    B --> C{key in transformProps?}
    C -->|yes| D[hasTransform = true, skip to style]
    C -->|no, other| G[style key = value]
    D --> H[continue loop]
    G --> H
    H --> B
    B --> I[All keys processed]
    I --> J{hasTranslate check — x or y in latestValues?}
    J -->|yes, NEW PATH| K[Enter buildTransform block]
    J -->|no| L{latestValues.transform absent?}
    L -->|absent| K
    L -->|present| M[Skip block — literal string stays]
    K --> N{hasTransform or transformTemplate?}
    N -->|yes| O[buildTransform runs — individual props win]
    N -->|no| P{no transform string and stale style.transform?}
    P -->|yes| Q[Reset style.transform to none]
    P -->|no| R[No change]
Loading

Comments Outside Diff (1)

  1. packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts, line 98-112 (link)

    P2 Duplicate test name

    Lines 82–96 and 98–112 both use the description "Applies transformTemplate if provided". Some test runners (e.g. Jest with --verbose) deduplicate or silently skip the second test, which means the coverage from the second case may not appear in CI reports. Consider renaming the second occurrence (e.g., "Applies transformTemplate with custom prefix") to ensure both cases are independently tracked.

Reviews (1): Last reviewed commit: "Stop variant `transform` strings from bl..." | Re-trigger Greptile

Comment on lines +58 to +59
const hasTranslate =
latestValues.x !== undefined || latestValues.y !== undefined
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The hasTranslate guard only checks x and y, but transformPropOrder also exposes z, translateX, translateY, and translateZ as valid motion value keys. If an element uses z-axis gestures, or if a consumer drives translateX/translateY directly instead of x/y, a literal transform string in latestValues would still silently win over those individual props — reproducing the same bug as #2807. Extending the check to cover those aliases keeps the fix consistent across all translate axes.

Suggested change
const hasTranslate =
latestValues.x !== undefined || latestValues.y !== undefined
const hasTranslate =
latestValues.x !== undefined ||
latestValues.y !== undefined ||
latestValues.z !== undefined ||
latestValues.translateX !== undefined ||
latestValues.translateY !== undefined ||
latestValues.translateZ !== undefined

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] The variants prevent drag transformation

1 participant