diff --git a/dev/react/src/tests/drag-variant-transform.tsx b/dev/react/src/tests/drag-variant-transform.tsx
new file mode 100644
index 0000000000..47a4994625
--- /dev/null
+++ b/dev/react/src/tests/drag-variant-transform.tsx
@@ -0,0 +1,48 @@
+import { motion, useDragControls, Variants } from "framer-motion"
+
+const variants: Variants = {
+ none: (custom?: string) => ({
+ width: "fit-content",
+ height: "fit-content",
+ transform: custom || undefined,
+ }),
+}
+
+// Regression test for #2807: a variant that sets a literal `transform` string
+// should not prevent drag from moving the element.
+export const App = () => {
+ const dragControls = useDragControls()
+
+ return (
+
+
+ )
+}
diff --git a/packages/framer-motion/cypress/integration/drag-variant-transform.ts b/packages/framer-motion/cypress/integration/drag-variant-transform.ts
new file mode 100644
index 0000000000..84db720b5f
--- /dev/null
+++ b/packages/framer-motion/cypress/integration/drag-variant-transform.ts
@@ -0,0 +1,36 @@
+/**
+ * Regression test for #2807: a variant that sets a literal `transform` string
+ * should not prevent drag from moving the element.
+ */
+describe("Drag with variant-set transform", () => {
+ it("Drags the element even when a variant sets a literal transform string", () => {
+ cy.visit("?test=drag-variant-transform")
+
+ // The variant applies `transform: "translate(50px, 60px)"` so the
+ // initial bounding rect reflects that translation (padding 100 + 50).
+ cy.get("[data-testid='draggable']")
+ .wait(200)
+ .then(($el: any) => {
+ expect(
+ $el[0].getBoundingClientRect().left,
+ "initial transform from variant is applied"
+ ).to.equal(150)
+ })
+
+ cy.get("[data-testid='handle']")
+ .trigger("pointerdown", 5, 5)
+ .trigger("pointermove", 10, 10)
+ .wait(50)
+ .trigger("pointermove", 200, 250, { force: true })
+ .wait(50)
+ .trigger("pointerup", { force: true })
+
+ cy.get("[data-testid='draggable']").should(($el: any) => {
+ const { left, top } = $el[0].getBoundingClientRect()
+ // Drag moved the pointer by ~195/245 px. The element should have
+ // moved by the same amount from its initial position.
+ expect(left, "left after drag").to.be.greaterThan(250)
+ expect(top, "top after drag").to.be.greaterThan(250)
+ })
+ })
+})
diff --git a/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts b/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts
index 643e91767a..ef116f16ee 100644
--- a/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts
+++ b/packages/framer-motion/src/render/html/utils/__tests__/build-styles.test.ts
@@ -110,6 +110,16 @@ describe("buildHTMLStyles", () => {
transform: "translateY(2) translateX(1px)",
})
})
+
+ test("Individual transform props take precedence over transform string", () => {
+ const latest = { x: 50, transform: "translateX(100px)" }
+ const style = {}
+ build(latest, { style })
+
+ expect(style).toEqual({
+ transform: "translateX(50px)",
+ })
+ })
})
interface BuildProps {
diff --git a/packages/motion-dom/src/render/html/utils/build-styles.ts b/packages/motion-dom/src/render/html/utils/build-styles.ts
index 5914df7876..d6627b2233 100644
--- a/packages/motion-dom/src/render/html/utils/build-styles.ts
+++ b/packages/motion-dom/src/render/html/utils/build-styles.ts
@@ -49,14 +49,23 @@ export function buildHTMLStyles(
}
}
- if (!latestValues.transform) {
+ /**
+ * If x or y motion values are present they take precedence over any
+ * `transform` string in latestValues. This allows drag (and other gestures
+ * that drive x/y) to keep working when a variant or style sets a literal
+ * `transform` string. See #2807.
+ */
+ const hasTranslate =
+ latestValues.x !== undefined || latestValues.y !== undefined
+
+ if (!latestValues.transform || hasTranslate) {
if (hasTransform || transformTemplate) {
style.transform = buildTransform(
latestValues,
state.transform,
transformTemplate
)
- } else if (style.transform) {
+ } else if (!latestValues.transform && style.transform) {
/**
* If we have previously created a transform but currently don't have any,
* reset transform style to none.