diff --git a/CHANGELOG.md b/CHANGELOG.md
index d697b28104..359915f5ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ Motion adheres to [Semantic Versioning](http://semver.org/).
Undocumented APIs should be considered internal and may change without warning.
+## [12.30.1] 2026-02-03
+
+### Fixed
+
+- Allow drag to be initiated by child `a` and `button` elements.
+
## [12.30.0] 2026-02-02
### Added
diff --git a/Makefile b/Makefile
index 3f9992ad27..ad1c34dc0a 100644
--- a/Makefile
+++ b/Makefile
@@ -95,7 +95,7 @@ test-e2e: test-nextjs test-html test-react test-react-19
yarn test-playwright
test-single: build test-mkdir
- yarn start-server-and-test "yarn dev-server" http://localhost:9991 "cd packages/framer-motion && cypress run --config-file=cypress.react-19.json --headed --spec cypress/integration/unit-conversion.ts"
+ yarn start-server-and-test "yarn dev-server" http://localhost:9991 "cd packages/framer-motion && cypress run --config-file=cypress.react.json --headed --spec cypress/integration/drag-nested.ts"
lint: bootstrap
yarn lint
diff --git a/dev/html/package.json b/dev/html/package.json
index d58ab3e796..d7d43cdd73 100644
--- a/dev/html/package.json
+++ b/dev/html/package.json
@@ -1,7 +1,7 @@
{
"name": "html-env",
"private": true,
- "version": "12.30.0",
+ "version": "12.30.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -10,9 +10,9 @@
"preview": "vite preview"
},
"dependencies": {
- "framer-motion": "^12.30.0",
- "motion": "^12.30.0",
- "motion-dom": "^12.30.0",
+ "framer-motion": "^12.30.1",
+ "motion": "^12.30.1",
+ "motion-dom": "^12.30.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/dev/next/package.json b/dev/next/package.json
index 215b096d23..26c138a4a3 100644
--- a/dev/next/package.json
+++ b/dev/next/package.json
@@ -1,7 +1,7 @@
{
"name": "next-env",
"private": true,
- "version": "12.30.0",
+ "version": "12.30.1",
"type": "module",
"scripts": {
"dev": "next dev",
@@ -10,7 +10,7 @@
"build": "next build"
},
"dependencies": {
- "motion": "^12.30.0",
+ "motion": "^12.30.1",
"next": "15.4.10",
"react": "19.0.0",
"react-dom": "19.0.0"
diff --git a/dev/react-19/package.json b/dev/react-19/package.json
index c2c1bbff12..e258ab4910 100644
--- a/dev/react-19/package.json
+++ b/dev/react-19/package.json
@@ -1,7 +1,7 @@
{
"name": "react-19-env",
"private": true,
- "version": "12.30.0",
+ "version": "12.30.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
- "motion": "^12.30.0",
+ "motion": "^12.30.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/dev/react/package.json b/dev/react/package.json
index 7b7e188e8b..aa89f7b533 100644
--- a/dev/react/package.json
+++ b/dev/react/package.json
@@ -1,7 +1,7 @@
{
"name": "react-env",
"private": true,
- "version": "12.30.0",
+ "version": "12.30.1",
"type": "module",
"scripts": {
"dev": "yarn vite",
@@ -11,7 +11,7 @@
"preview": "yarn vite preview"
},
"dependencies": {
- "framer-motion": "^12.30.0",
+ "framer-motion": "^12.30.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/lerna.json b/lerna.json
index 8342924aa4..5ac1a87dcd 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "12.30.0",
+ "version": "12.30.1",
"packages": [
"packages/*",
"dev/*"
diff --git a/packages/framer-motion/cypress/integration/drag-input-propagation.ts b/packages/framer-motion/cypress/integration/drag-input-propagation.ts
index 0fafbb74e3..26dfe60725 100644
--- a/packages/framer-motion/cypress/integration/drag-input-propagation.ts
+++ b/packages/framer-motion/cypress/integration/drag-input-propagation.ts
@@ -1,6 +1,11 @@
/**
- * Tests for issue #1674: Interactive elements inside draggable elements should not trigger drag
- * https://github.com/motiondivision/motion/issues/1674
+ * Tests for drag behavior with interactive elements inside draggable elements.
+ *
+ * Form controls where text selection or direct interaction is expected
+ * (input, textarea, select, contenteditable) should NOT trigger drag.
+ *
+ * Buttons and links SHOULD allow drag since they don't have click-and-move
+ * actions of their own.
*/
describe("Drag Input Propagation", () => {
it("Should not drag when clicking and dragging on an input inside draggable", () => {
@@ -60,7 +65,7 @@ describe("Drag Input Propagation", () => {
})
})
- it("Should not drag when clicking and dragging on a button inside draggable", () => {
+ it("Should drag when clicking and dragging on a button inside draggable", () => {
cy.visit("?test=drag-input-propagation")
.wait(200)
.get("[data-testid='draggable']")
@@ -70,7 +75,7 @@ describe("Drag Input Propagation", () => {
expect(top).to.equal(100)
})
- // Attempt to drag by clicking on the button
+ // Drag by clicking on the button - buttons don't have click-and-move actions
cy.get("[data-testid='button']")
.trigger("pointerdown", 5, 5)
.trigger("pointermove", 10, 10)
@@ -79,15 +84,16 @@ describe("Drag Input Propagation", () => {
.wait(50)
.trigger("pointerup", { force: true })
- // Verify the draggable element did NOT move
+ // Verify the draggable element DID move
cy.get("[data-testid='draggable']").should(($draggable) => {
const { left, top } = $draggable[0].getBoundingClientRect()
- expect(left).to.equal(100)
- expect(top).to.equal(100)
+ // Element should have moved
+ expect(left).to.be.greaterThan(200)
+ expect(top).to.be.greaterThan(200)
})
})
- it("Should not drag when clicking and dragging on a link inside draggable", () => {
+ it("Should drag when clicking and dragging on a link inside draggable", () => {
cy.visit("?test=drag-input-propagation")
.wait(200)
.get("[data-testid='draggable']")
@@ -97,7 +103,7 @@ describe("Drag Input Propagation", () => {
expect(top).to.equal(100)
})
- // Attempt to drag by clicking on the link
+ // Drag by clicking on the link - links don't have click-and-move actions
cy.get("[data-testid='link']")
.trigger("pointerdown", 5, 5)
.trigger("pointermove", 10, 10)
@@ -106,11 +112,12 @@ describe("Drag Input Propagation", () => {
.wait(50)
.trigger("pointerup", { force: true })
- // Verify the draggable element did NOT move
+ // Verify the draggable element DID move
cy.get("[data-testid='draggable']").should(($draggable) => {
const { left, top } = $draggable[0].getBoundingClientRect()
- expect(left).to.equal(100)
- expect(top).to.equal(100)
+ // Element should have moved
+ expect(left).to.be.greaterThan(200)
+ expect(top).to.be.greaterThan(200)
})
})
diff --git a/packages/framer-motion/cypress/integration/drag-nested.ts b/packages/framer-motion/cypress/integration/drag-nested.ts
index d619134f2f..c0b6d3999b 100644
--- a/packages/framer-motion/cypress/integration/drag-nested.ts
+++ b/packages/framer-motion/cypress/integration/drag-nested.ts
@@ -372,9 +372,9 @@ function testAlternateAxes(parentLayout: boolean, childLayout: boolean) {
.wait(200)
.get("#child")
.trigger("pointerdown", 5, 5, { force: true })
- .wait(50)
+ .wait(80)
.trigger("pointermove", 10, 10, { force: true })
- .wait(50)
+ .wait(80)
.trigger("pointermove", 100, 100, { force: true })
.wait(80)
.should(([$child]: any) => {
diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json
index a73f08dde4..ca60828950 100644
--- a/packages/framer-motion/package.json
+++ b/packages/framer-motion/package.json
@@ -1,6 +1,6 @@
{
"name": "framer-motion",
- "version": "12.30.0",
+ "version": "12.30.1",
"description": "A simple and powerful JavaScript animation library",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
@@ -88,7 +88,7 @@
"measure": "rollup -c ./rollup.size.config.mjs"
},
"dependencies": {
- "motion-dom": "^12.30.0",
+ "motion-dom": "^12.30.1",
"motion-utils": "^12.29.2",
"tslib": "^2.4.0"
},
diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts
index 5a2001996b..d19b302c4c 100644
--- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts
+++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts
@@ -7,7 +7,7 @@ import {
createBox,
eachAxis,
frame,
- isElementKeyboardAccessible,
+ isElementTextInput,
measurePageBox,
mixNumber,
PanInfo,
@@ -655,15 +655,17 @@ export class VisualElementDragControls {
const target = event.target as Element
/**
- * Only block drag if clicking on a keyboard-accessible child element.
- * If the draggable element itself is keyboard-accessible (e.g., motion.button),
- * dragging should still work when clicking directly on it.
+ * Only block drag if clicking on a text input child element
+ * (input, textarea, select, contenteditable) where users might
+ * want to select text or interact with the control.
+ *
+ * Buttons and links don't block drag since they don't have
+ * click-and-move actions of their own.
*/
- const isClickingKeyboardAccessibleChild =
- target !== element &&
- isElementKeyboardAccessible(target)
+ const isClickingTextInputChild =
+ target !== element && isElementTextInput(target)
- if (drag && dragListener && !isClickingKeyboardAccessibleChild) {
+ if (drag && dragListener && !isClickingTextInputChild) {
this.start(event)
}
}
diff --git a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx
index f2607d493c..75aea1ea18 100644
--- a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx
+++ b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx
@@ -1004,7 +1004,60 @@ describe("keyboard accessible elements", () => {
expect(x.get()).toBeGreaterThanOrEqual(100)
})
- test("drag gesture does not start when clicking a child button", async () => {
+ test("drag gesture starts on a motion.input with drag prop", async () => {
+ const onDragStart = jest.fn()
+ const x = motionValue(0)
+ const Component = () => (
+
+
+
+ )
+
+ const { getByTestId, rerender } = render()
+ rerender()
+
+ const pointer = await drag(getByTestId("draggable-input")).to(100, 100)
+ pointer.end()
+
+ await nextFrame()
+
+ expect(onDragStart).toBeCalledTimes(1)
+ expect(x.get()).toBeGreaterThanOrEqual(100)
+ })
+
+ test("drag gesture starts on a motion.a with drag prop", async () => {
+ const onDragStart = jest.fn()
+ const x = motionValue(0)
+ const Component = () => (
+
+
+
+ )
+
+ const { getByTestId, rerender } = render()
+ rerender()
+
+ const pointer = await drag(getByTestId("draggable-link")).to(100, 100)
+ pointer.end()
+
+ await nextFrame()
+
+ expect(onDragStart).toBeCalledTimes(1)
+ expect(x.get()).toBeGreaterThanOrEqual(100)
+ })
+
+ test("drag gesture does not start when clicking a child input", async () => {
const onDragStart = jest.fn()
const x = motionValue(0)
const Component = () => (
@@ -1015,7 +1068,7 @@ describe("keyboard accessible elements", () => {
onDragStart={onDragStart}
style={{ x }}
>
-
+
)
@@ -1025,7 +1078,7 @@ describe("keyboard accessible elements", () => {
const pointer = await drag(
getByTestId("draggable"),
- getByTestId("child-button")
+ getByTestId("child-input")
).to(100, 100)
pointer.end()
@@ -1035,51 +1088,93 @@ describe("keyboard accessible elements", () => {
expect(x.get()).toBe(0)
})
- test("drag gesture starts on a motion.input with drag prop", async () => {
+ test("drag gesture does not start when clicking a child textarea", async () => {
const onDragStart = jest.fn()
const x = motionValue(0)
const Component = () => (
-
+ >
+
+
)
const { getByTestId, rerender } = render()
rerender()
- const pointer = await drag(getByTestId("draggable-input")).to(100, 100)
+ const pointer = await drag(
+ getByTestId("draggable"),
+ getByTestId("child-textarea")
+ ).to(100, 100)
pointer.end()
await nextFrame()
- expect(onDragStart).toBeCalledTimes(1)
- expect(x.get()).toBeGreaterThanOrEqual(100)
+ expect(onDragStart).toBeCalledTimes(0)
+ expect(x.get()).toBe(0)
})
- test("drag gesture starts on a motion.a with drag prop", async () => {
+ test("drag gesture does not start when clicking a child select", async () => {
const onDragStart = jest.fn()
const x = motionValue(0)
const Component = () => (
-
+ >
+
+
)
const { getByTestId, rerender } = render()
rerender()
- const pointer = await drag(getByTestId("draggable-link")).to(100, 100)
+ const pointer = await drag(
+ getByTestId("draggable"),
+ getByTestId("child-select")
+ ).to(100, 100)
+ pointer.end()
+
+ await nextFrame()
+
+ expect(onDragStart).toBeCalledTimes(0)
+ expect(x.get()).toBe(0)
+ })
+
+ test("drag gesture starts when clicking a child button", async () => {
+ const onDragStart = jest.fn()
+ const x = motionValue(0)
+ const Component = () => (
+
+
+
+
+
+ )
+
+ const { getByTestId, rerender } = render()
+ rerender()
+
+ const pointer = await drag(
+ getByTestId("draggable"),
+ getByTestId("child-button")
+ ).to(100, 100)
pointer.end()
await nextFrame()
@@ -1088,7 +1183,7 @@ describe("keyboard accessible elements", () => {
expect(x.get()).toBeGreaterThanOrEqual(100)
})
- test("drag gesture does not start when clicking a child input", async () => {
+ test("drag gesture starts when clicking a child link", async () => {
const onDragStart = jest.fn()
const x = motionValue(0)
const Component = () => (
@@ -1099,7 +1194,9 @@ describe("keyboard accessible elements", () => {
onDragStart={onDragStart}
style={{ x }}
>
-
+
+ Click me
+
)
@@ -1109,13 +1206,13 @@ describe("keyboard accessible elements", () => {
const pointer = await drag(
getByTestId("draggable"),
- getByTestId("child-input")
+ getByTestId("child-link")
).to(100, 100)
pointer.end()
await nextFrame()
- expect(onDragStart).toBeCalledTimes(0)
- expect(x.get()).toBe(0)
+ expect(onDragStart).toBeCalledTimes(1)
+ expect(x.get()).toBeGreaterThanOrEqual(100)
})
})
diff --git a/packages/motion-dom/package.json b/packages/motion-dom/package.json
index 00e2bbfbc0..429f92a9b2 100644
--- a/packages/motion-dom/package.json
+++ b/packages/motion-dom/package.json
@@ -1,6 +1,6 @@
{
"name": "motion-dom",
- "version": "12.30.0",
+ "version": "12.30.1",
"author": "Matt Perry",
"license": "MIT",
"repository": "https://github.com/motiondivision/motion",
diff --git a/packages/motion-dom/src/gestures/press/utils/is-keyboard-accessible.ts b/packages/motion-dom/src/gestures/press/utils/is-keyboard-accessible.ts
index a22b9be82d..0e60269369 100644
--- a/packages/motion-dom/src/gestures/press/utils/is-keyboard-accessible.ts
+++ b/packages/motion-dom/src/gestures/press/utils/is-keyboard-accessible.ts
@@ -1,4 +1,4 @@
-const interactiveElements = new Set([
+const keyboardAccessibleElements = new Set([
"BUTTON",
"INPUT",
"SELECT",
@@ -7,16 +7,32 @@ const interactiveElements = new Set([
])
/**
- * Checks if an element is an interactive form element that should prevent
- * drag gestures from starting when clicked.
- *
- * This specifically targets form controls, buttons, and links - not just any
- * element with tabIndex, since motion elements with tap handlers automatically
- * get tabIndex=0 for keyboard accessibility.
+ * Checks if an element is natively keyboard accessible (focusable).
+ * Used by the press gesture to determine if we need to add tabIndex.
*/
export function isElementKeyboardAccessible(element: Element) {
return (
- interactiveElements.has(element.tagName) ||
+ keyboardAccessibleElements.has(element.tagName) ||
+ (element as HTMLElement).isContentEditable === true
+ )
+}
+
+const textInputElements = new Set(["INPUT", "SELECT", "TEXTAREA"])
+
+/**
+ * Checks if an element has text selection or direct interaction behavior
+ * that should block drag gestures from starting.
+ *
+ * This specifically targets form controls where the user might want to select
+ * text or interact with the control (e.g., sliders, dropdowns).
+ *
+ * Buttons and links are NOT included because they don't have click-and-move
+ * actions of their own - they only respond to click events, so dragging
+ * should still work when initiated from these elements.
+ */
+export function isElementTextInput(element: Element) {
+ return (
+ textInputElements.has(element.tagName) ||
(element as HTMLElement).isContentEditable === true
)
}
diff --git a/packages/motion/package.json b/packages/motion/package.json
index d3d990aba0..ba1ead3ab4 100644
--- a/packages/motion/package.json
+++ b/packages/motion/package.json
@@ -1,6 +1,6 @@
{
"name": "motion",
- "version": "12.30.0",
+ "version": "12.30.1",
"description": "An animation library for JavaScript and React.",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
@@ -76,7 +76,7 @@
"postpublish": "git push --tags"
},
"dependencies": {
- "framer-motion": "^12.30.0",
+ "framer-motion": "^12.30.1",
"tslib": "^2.4.0"
},
"peerDependencies": {
diff --git a/yarn.lock b/yarn.lock
index 1a3f3495c7..1830c98867 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7420,14 +7420,14 @@ __metadata:
languageName: node
linkType: hard
-"framer-motion@^12.30.0, framer-motion@workspace:packages/framer-motion":
+"framer-motion@^12.30.1, framer-motion@workspace:packages/framer-motion":
version: 0.0.0-use.local
resolution: "framer-motion@workspace:packages/framer-motion"
dependencies:
"@radix-ui/react-dialog": ^1.1.15
"@thednp/dommatrix": ^2.0.11
"@types/three": 0.137.0
- motion-dom: ^12.30.0
+ motion-dom: ^12.30.1
motion-utils: ^12.29.2
three: 0.137.0
tslib: ^2.4.0
@@ -8192,9 +8192,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "html-env@workspace:dev/html"
dependencies:
- framer-motion: ^12.30.0
- motion: ^12.30.0
- motion-dom: ^12.30.0
+ framer-motion: ^12.30.1
+ motion: ^12.30.1
+ motion-dom: ^12.30.1
react: ^18.3.1
react-dom: ^18.3.1
vite: ^5.2.0
@@ -10936,7 +10936,7 @@ __metadata:
languageName: node
linkType: hard
-"motion-dom@^12.30.0, motion-dom@workspace:packages/motion-dom":
+"motion-dom@^12.30.1, motion-dom@workspace:packages/motion-dom":
version: 0.0.0-use.local
resolution: "motion-dom@workspace:packages/motion-dom"
dependencies:
@@ -11013,11 +11013,11 @@ __metadata:
languageName: unknown
linkType: soft
-"motion@^12.30.0, motion@workspace:packages/motion":
+"motion@^12.30.1, motion@workspace:packages/motion":
version: 0.0.0-use.local
resolution: "motion@workspace:packages/motion"
dependencies:
- framer-motion: ^12.30.0
+ framer-motion: ^12.30.1
tslib: ^2.4.0
peerDependencies:
"@emotion/is-prop-valid": "*"
@@ -11134,7 +11134,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "next-env@workspace:dev/next"
dependencies:
- motion: ^12.30.0
+ motion: ^12.30.1
next: 15.4.10
react: 19.0.0
react-dom: 19.0.0
@@ -12599,7 +12599,7 @@ __metadata:
"@typescript-eslint/parser": ^7.2.0
"@vitejs/plugin-react-swc": ^3.5.0
eslint-plugin-react-refresh: ^0.4.6
- motion: ^12.30.0
+ motion: ^12.30.1
react: ^19.0.0
react-dom: ^19.0.0
vite: ^5.2.0
@@ -12683,7 +12683,7 @@ __metadata:
"@typescript-eslint/parser": ^7.2.0
"@vitejs/plugin-react-swc": ^3.5.0
eslint-plugin-react-refresh: ^0.4.6
- framer-motion: ^12.30.0
+ framer-motion: ^12.30.1
react: ^18.3.1
react-dom: ^18.3.1
vite: ^5.2.0