Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3d5cab4
Fix spring timeline with multiple segments using defaultTransition
claude Jan 19, 2026
c092db9
Fix drag gesture cleanup when component unmounts during active pan se…
claude Jan 19, 2026
06e47e4
Add yarn cache files for resolve and typescript patches
claude Jan 19, 2026
13fbdd8
fix: use unitless stroke-dash values to fix Safari zoom bug
claude Jan 19, 2026
90161c1
Fix null check for document.body in measureScroll
claude Jan 19, 2026
8589aac
Add useAnimatedValue hook for generic motion transitions
claude Jan 19, 2026
861f5db
Refactor useSpring and springValue to use useAnimatedValue internally
claude Jan 19, 2026
56ea968
Fix AnimatePresence popLayout mode shifting elements with bottom posi…
claude Jan 19, 2026
eb5a47c
Fix drag gesture on keyboard-accessible elements with drag prop
claude Jan 19, 2026
a9aa1f1
Remove unused keyframes variable in test
claude Jan 20, 2026
c62e4a7
Fix test to not depend on projection node instantiation
claude Jan 20, 2026
a189106
test: update SSR test expectations for unitless stroke-dash values
claude Jan 20, 2026
c38cee1
Fix docs typo in use-presence.ts
NoelDeMartin Jan 20, 2026
7bc7258
Fix drag cleanup to only end pan session, preserving nested drag beha…
claude Jan 20, 2026
9b11547
Rename useAnimatedValue to useFollowValue
claude Jan 20, 2026
5938c6d
Merge pull request #3488 from NoelDeMartin/patch-1
mattgperry Jan 20, 2026
c4b4b5f
Fix TypeScript overload signature in useSpring
claude Jan 20, 2026
ae4e842
Merge pull request #3480 from motiondivision/claude/fix-drag-keyboard…
mattgperry Jan 20, 2026
27a85c4
Updating changelog
mattgperry Jan 20, 2026
17250ff
v12.27.3
mattgperry Jan 20, 2026
0c5387e
test: update SVG path test expectations for unitless stroke-dash values
claude Jan 20, 2026
4332b3c
Fix spring type and properties inheritance from defaultTransition
claude Jan 20, 2026
50a8231
Merge pull request #3489 from motiondivision/claude/fix-issue-3324-4P50v
mattgperry Jan 20, 2026
e0a022d
Merge pull request #3484 from motiondivision/claude/fix-issue-3301-Ddb4a
mattgperry Jan 20, 2026
d762100
Updating changelog
mattgperry Jan 20, 2026
8fb8837
v12.27.4
mattgperry Jan 20, 2026
a5263db
Updating svgEffect to use unitless path values
mattgperry Jan 20, 2026
e8ccc93
Undoing disable default value type
mattgperry Jan 20, 2026
4cfd9db
Merge pull request #3483 from motiondivision/claude/fix-issue-3348-BH6yV
mattgperry Jan 20, 2026
50bfd58
Merge pull request #3482 from motiondivision/claude/fix-motion-issue-…
mattgperry Jan 20, 2026
c1dd63e
Updating changelog
mattgperry Jan 20, 2026
d95d273
v12.27.5
mattgperry Jan 20, 2026
44a50ff
Remove flaky tween animation test
claude Jan 20, 2026
efffa91
Merge pull request #3477 from motiondivision/claude/fix-timeline-quot…
mattgperry Jan 20, 2026
1c88c2b
Merge pull request #3485 from motiondivision/claude/create-useanimate…
mattgperry Jan 20, 2026
fe209d4
Updating changelog
mattgperry Jan 20, 2026
2a38732
Updating changelog
mattgperry Jan 20, 2026
5e20a87
v12.28.0
mattgperry Jan 20, 2026
9bfdcd1
Updating new script
mattgperry Jan 20, 2026
9a673e4
Fix flaky drag-nested Cypress tests by increasing wait times
mattgperry Jan 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@ Motion adheres to [Semantic Versioning](http://semver.org/).

Undocumented APIs should be considered internal and may change without warning.

## [12.28.0] 2026-01-20

### Added

- `useFollowValue` and `followValue`: `useSpring`-style motion values that can accept any transition.

### Fixed

- Fix "multiple keyframe" error when using spring animations as default transitions for animation sequences.

## [12.27.5] 2026-01-20

### Fixed

- Ensure pen gesture is correctly cleaned up on drag cancel.
- Fix edge case where `DocumentProjectionNode`'s attached element was `null`.

## [12.27.4] 2026-01-20

### Added

- `AnimatePresence`: `anchorY` for vertically positioning popped children.

### Fixed

- Fixed path drawing animations in zoomed Safari contexts by switching to unitless values.

## [12.27.3] 2026-01-20

### Fixed

- Ensure drag gestures trigger from keyboard-accessible elements (`button`, `textarea` etc) if these elements have `drag` applied directly.

## [12.27.2] 2026-01-20

### Fixed
Expand Down
8 changes: 4 additions & 4 deletions dev/html/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "html-env",
"private": true,
"version": "12.27.2",
"version": "12.28.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -10,9 +10,9 @@
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^12.27.2",
"motion": "^12.27.2",
"motion-dom": "^12.27.2",
"framer-motion": "^12.28.0",
"motion": "^12.28.0",
"motion-dom": "^12.28.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
4 changes: 2 additions & 2 deletions dev/next/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "next-env",
"private": true,
"version": "12.27.2",
"version": "12.28.0",
"type": "module",
"scripts": {
"dev": "next dev",
Expand All @@ -10,7 +10,7 @@
"build": "next build"
},
"dependencies": {
"motion": "^12.27.2",
"motion": "^12.28.0",
"next": "15.4.10",
"react": "19.0.0",
"react-dom": "19.0.0"
Expand Down
4 changes: 2 additions & 2 deletions dev/react-19/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-19-env",
"private": true,
"version": "12.27.2",
"version": "12.28.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand All @@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"motion": "^12.27.2",
"motion": "^12.28.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
Expand Down
4 changes: 2 additions & 2 deletions dev/react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-env",
"private": true,
"version": "12.27.2",
"version": "12.28.0",
"type": "module",
"scripts": {
"dev": "yarn vite",
Expand All @@ -11,7 +11,7 @@
"preview": "yarn vite preview"
},
"dependencies": {
"framer-motion": "^12.27.2",
"framer-motion": "^12.28.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "12.27.2",
"version": "12.28.0",
"packages": [
"packages/*",
"dev/*"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"dev-server": "turbo run dev-server",
"lint": "turbo run lint",
"test": "turbo run test",
"test-playwright": "yarn playwright test",
"test-playwright": "turbo run build && yarn playwright test",
"test-ci": "turbo run test-ci --no-cache",
"measure": "turbo run measure --force && node dev/inc/bundlesize.mjs",
"version": "yarn install && git stage yarn.lock",
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
["basic-position-change.html","interrupt-animation.html","repeat-animation.html","scale-correction.html","scope-with-data-layout.html","shared-element-a-ab-a.html","shared-element-a-b-a-replace.html","shared-element-a-b-a-reuse.html","shared-element-app-store.html","shared-element-basic.html","shared-element-configured.html","shared-element-crossfade.html","shared-element-nested-children-bottom.html","shared-element-nested-children.html","shared-element-no-crossfade.html","shared-multiple-elements.html"]
["app-store-a-b-a.html","basic-position-change.html","interrupt-animation.html","repeat-animation.html","scale-correction.html","scope-with-data-layout.html","shared-element-a-ab-a.html","shared-element-a-b-a-replace.html","shared-element-a-b-a-reuse.html","shared-element-basic.html","shared-element-configured.html","shared-element-crossfade.html","shared-element-nested-children-bottom.html","shared-element-nested-children.html","shared-element-no-crossfade.html","shared-multiple-elements.html"]
8 changes: 5 additions & 3 deletions packages/framer-motion/cypress/integration/drag-nested.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function testNestedDrag(parentLayout: boolean, childLayout: boolean) {
if (childLayout) url += `&childLayout=true`

cy.visit(url)
.wait(50)
.wait(200)
.get("#parent")
.should(([$parent]: any) => {
expectBbox($parent, {
Expand Down Expand Up @@ -128,7 +128,7 @@ function testNestedDrag(parentLayout: boolean, childLayout: boolean) {
})
.get("#parent")
.trigger("pointerdown", 5, 5)
.wait(20)
.wait(50)
.trigger("pointermove", 10, 10) // Gesture will start from first move past threshold
.wait(50)
.trigger("pointermove", 50, 50)
Expand Down Expand Up @@ -174,6 +174,7 @@ function testNestedDragConstraints(
if (childLayout) url += `&childLayout=true`

cy.visit(url)
.wait(200)
.get("#parent")
.trigger("pointerdown", 40, 40)
.wait(50)
Expand Down Expand Up @@ -280,6 +281,7 @@ function testNestedDragConstraintsAndAnimation(
if (parentLayout) url += `&parentLayout=true`
if (childLayout) url += `&childLayout=true`
cy.visit(url)
.wait(200)
.get("#parent")
.trigger("pointerdown", 5, 10)
.wait(50)
Expand Down Expand Up @@ -367,7 +369,7 @@ function testAlternateAxes(parentLayout: boolean, childLayout: boolean) {
if (childLayout) url += `&childLayout=true`
return cy
.visit(url)
.wait(50)
.wait(200)
.get("#child")
.trigger("pointerdown", 5, 5, { force: true })
.wait(50)
Expand Down
4 changes: 2 additions & 2 deletions packages/framer-motion/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "framer-motion",
"version": "12.27.2",
"version": "12.28.0",
"description": "A simple and powerful JavaScript animation library",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
Expand Down Expand Up @@ -88,7 +88,7 @@
"measure": "rollup -c ./rollup.size.config.mjs"
},
"dependencies": {
"motion-dom": "^12.27.2",
"motion-dom": "^12.28.0",
"motion-utils": "^12.27.2",
"tslib": "^2.4.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,31 @@ describe("createAnimationsFromSequence", () => {
expect(times).toEqual([0, 0.45454545454545453, 0.45454545454545453, 1])
})

test("Does not include type: spring in transition when spring is converted to easing via defaultTransition", () => {
const animations = createAnimationsFromSequence(
[
[a, { x: 0 }, { duration: 0 }],
[a, { x: 1.12 }],
[a, { x: 0.98 }, { at: "<+0.15" }],
[a, { x: 1 }, { at: "<+0.35" }],
],
{ defaultTransition: { type: "spring", stiffness: 72, damping: 10 } },
undefined,
{ spring }
)

const { transition } = animations.get(a)!

// The spring should be converted to easing functions, not kept as type: "spring"
expect(transition.x.type).toBeUndefined()

// Verify the easing functions are present
expect(Array.isArray(transition.x.ease)).toBe(true)
const easeArray = transition.x.ease as Easing[]
// At least some of the easings should be spring-converted functions
expect(easeArray.some((e) => typeof e === "function")).toBe(true)
})

test("It correctly repeats keyframes once", () => {
const animations = createAnimationsFromSequence(
[[a, { x: [0, 100] }, { duration: 1, repeat: 1, ease: "linear" }]],
Expand Down
18 changes: 15 additions & 3 deletions packages/framer-motion/src/animation/sequence/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function createAnimationsFromSequence(
const {
delay = 0,
times = defaultOffset(valueKeyframesAsList),
type = "keyframes",
type = defaultTransition.type || "keyframes",
repeat,
repeatType,
repeatDelay = 0,
Expand Down Expand Up @@ -151,7 +151,10 @@ export function createAnimationsFromSequence(
absoluteDelta = Math.abs(delta)
}

const springTransition = { ...remainingTransition }
const springTransition = {
...defaultTransition,
...remainingTransition,
}
if (duration !== undefined) {
springTransition.duration = secondsToMilliseconds(duration)
}
Expand Down Expand Up @@ -368,8 +371,17 @@ export function createAnimationsFromSequence(
const definition = animationDefinitions.get(element)!

definition.keyframes[key] = keyframes

/**
* Exclude `type` from defaultTransition since springs have been
* converted to duration-based easing functions in resolveValueSequence.
* Including `type: "spring"` would cause JSAnimation to error when
* the merged keyframes array has more than 2 keyframes.
*/
const { type: _type, ...remainingDefaultTransition } =
defaultTransition
definition.transition[key] = {
...defaultTransition,
...remainingDefaultTransition,
duration: totalDuration,
ease: valueEasing,
times: valueOffset,
Expand Down
14 changes: 11 additions & 3 deletions packages/framer-motion/src/components/AnimatePresence/PopChild.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ interface Size {
top: number
left: number
right: number
bottom: number
}

interface Props {
children: React.ReactElement
isPresent: boolean
anchorX?: "left" | "right"
anchorY?: "top" | "bottom"
root?: HTMLElement | ShadowRoot
}

Expand All @@ -39,13 +41,17 @@ class PopChildMeasure extends React.Component<MeasureProps> {
const parentWidth = isHTMLElement(parent)
? parent.offsetWidth || 0
: 0
const parentHeight = isHTMLElement(parent)
? parent.offsetHeight || 0
: 0

const size = this.props.sizeRef.current!
size.height = element.offsetHeight || 0
size.width = element.offsetWidth || 0
size.top = element.offsetTop
size.left = element.offsetLeft
size.right = parentWidth - size.width - size.left
size.bottom = parentHeight - size.height - size.top
}

return null
Expand All @@ -61,7 +67,7 @@ class PopChildMeasure extends React.Component<MeasureProps> {
}
}

export function PopChild({ children, isPresent, anchorX, root }: Props) {
export function PopChild({ children, isPresent, anchorX, anchorY, root }: Props) {
const id = useId()
const ref = useRef<HTMLElement>(null)
const size = useRef<Size>({
Expand All @@ -70,6 +76,7 @@ export function PopChild({ children, isPresent, anchorX, root }: Props) {
top: 0,
left: 0,
right: 0,
bottom: 0,
})
const { nonce } = useContext(MotionConfigContext)
/**
Expand All @@ -91,10 +98,11 @@ export function PopChild({ children, isPresent, anchorX, root }: Props) {
* styles set via the style prop.
*/
useInsertionEffect(() => {
const { width, height, top, left, right } = size.current
const { width, height, top, left, right, bottom } = size.current
if (isPresent || !ref.current || !width || !height) return

const x = anchorX === "left" ? `left: ${left}` : `right: ${right}`
const y = anchorY === "bottom" ? `bottom: ${bottom}` : `top: ${top}`

ref.current.dataset.motionPopId = id

Expand All @@ -111,7 +119,7 @@ export function PopChild({ children, isPresent, anchorX, root }: Props) {
width: ${width}px !important;
height: ${height}px !important;
${x}px !important;
top: ${top}px !important;
${y}px !important;
}
`)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface PresenceChildProps {
presenceAffectsLayout: boolean
mode: "sync" | "popLayout" | "wait"
anchorX?: "left" | "right"
anchorY?: "top" | "bottom"
root?: HTMLElement | ShadowRoot
}

Expand All @@ -31,6 +32,7 @@ export const PresenceChild = ({
presenceAffectsLayout,
mode,
anchorX,
anchorY,
root
}: PresenceChildProps) => {
const presenceChildren = useConstant(newChildrenMap)
Expand Down Expand Up @@ -86,7 +88,7 @@ export const PresenceChild = ({

if (mode === "popLayout") {
children = (
<PopChild isPresent={isPresent} anchorX={anchorX} root={root}>
<PopChild isPresent={isPresent} anchorX={anchorX} anchorY={anchorY} root={root}>
{children}
</PopChild>
)
Expand Down
Loading
Loading