plans/issues/issue-1725.md
transition.out (PR #2951) to v12Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving on. If anything in "STOP conditions" occurs, stop and report — do not improvise. When done, update this plan's row in
plans/issues/README.md.Drift check (run first):
gh api repos/motiondivision/motion/issues/1725 --jq '.state'→open(if closed, mark DONE and stop).grep -n "out?: boolean\|nextTransition" packages/motion-dom/src/animation/types.ts packages/motion-dom/src/value/index.ts— if matches, the feature already landed; skip to Step 6.git diff --stat 42bfbe3ed..HEAD -- packages/motion-dom/src/animation/interfaces/visual-element-target.ts packages/motion-dom/src/value/index.ts— drift in these files ⇒ compare against the excerpts below; mismatch on the quoted regions = STOP.
42bfbe3ed, 2026-06-11Filed by the maintainer himself (2022): there's no way to define a transition
for leaving a state — animate={{ scale: 1, transition: { delay: 1 } }}
re-applies the delay when returning from whileHover, because the entered
variant always defines the transition. The thread converged (maintainer,
2025-01-21): "This is the closest as I think I'll implement
https://github.com/motiondivision/motion/pull/2951" — transition.out.
PR #2951 was closed unmerged 2025-07-01 (stale against the v12 motion-dom
migration, not rejected on design). Issue #2636 ("whileInView transition
values override other transition values") is the same pain reported as a bug;
landing this resolves both.
transitionFrom) — closed.transition.out — maintainer's own, branch
origin/feature/transition-out still exists (head b6ffa460c). API:
<motion.div
animate={{ opacity: 0, transition: { delay: 1 } }}
whileHover={{ opacity: 1, transition: { out: true, duration: 1 } }}
/>
// leaving hover uses { duration: 1 } — not animate's delay
5 files, ~80 lines of logic + 100 lines of tests:
Transition gains out?: boolean (was
packages/framer-motion/src/animation/types.ts; in v12 the Transition
interface lives in packages/motion-dom/src/animation/types.ts).MotionValue gains nextTransition?: Transition (was
packages/framer-motion/src/value/index.ts; in v12:
packages/motion-dom/src/value/index.ts).animateTarget consumes/stores it (was
packages/framer-motion/src/animation/interfaces/visual-element-target.ts;
in v12: packages/motion-dom/src/animation/interfaces/visual-element-target.ts,
whose per-key loop at lines 73-91 currently builds
const valueTransition = { delay, ...getValueTransition(transition || {}, key) }).
The branch inserts, immediately after building valueTransition:
let outTransition: Transition | undefined
if (type && value.nextTransition) {
outTransition = value.nextTransition
}
value.nextTransition = undefined
if (valueTransition.out) {
value.nextTransition = valueTransition
}
if (outTransition) {
valueTransition = outTransition
}
packages/framer-motion/src/motion/__tests__/transition-out.test.tsx
(100 lines on the branch — port them).dev/react/src/examples/Animation-transition-out.tsx.| Purpose | Command | Expected |
|---|---|---|
| Fetch branch | git fetch origin feature/transition-out | exit 0 |
| Full branch diff | MB=$(git merge-base origin/feature/transition-out main); git diff $MB...origin/feature/transition-out | the 5-file diff above |
| Build | yarn build (repo root) | exit 0 |
| New tests | npx jest --config packages/framer-motion/jest.config.json --testPathPattern="transition-out" | pass |
| Regression sweep | cd packages/framer-motion && yarn test-client | no new failures |
The design was chosen by the maintainer, but he also closed his own PR — so
confirm: set this plan's row in plans/issues/README.md to APPROVED
(port transition.out as specified) or REJECTED (leave the issue open
and report; do NOT close #1725 as not_planned — it's the maintainer's own
roadmap item).
Copy transition-out.test.tsx from the branch
(git show origin/feature/transition-out:packages/framer-motion/src/motion/__tests__/transition-out.test.tsx)
into the same path on your branch. Fix imports for v12 (e.g. types now come
through motion-dom re-exports). Run the transition-out filter → tests FAIL
(feature absent) — the right failure reason for a feature port.
Apply changes 1-3 from "Mechanism" at the v12 locations. Notes for the executor:
visual-element-target.ts the loop also has the
"skip if already at target" early-continue (lines 100-110) and the
skipAnimations flag — insert the out block right after
const valueTransition = {...} (line 88-91) and before those, mirroring
the branch's placement after the transition is built.type is the VisualElementAnimationOptions["type"] param already
destructured at line 36 — the gate if (type && value.nextTransition)
restricts out consumption to variant-driven animations; keep it.out?: boolean JSDoc from the branch verbatim (it's good docs).Verify: transition-out filter → all pass.
Add one more test (same file): whileInView-style variant with
transition: { delay: 3 } + whileHover with
transition: { out: true, duration: 0.3 }; assert leaving hover animates
back without the 3s delay. (JSDOM can't do real IntersectionObserver — drive
it with animate/whileHover props the way the existing hover tests in
packages/framer-motion/src/gestures/__tests__/hover.test.tsx do.)
yarn build → exit 0. cd packages/framer-motion && yarn test-client → no
new failures vs a baseline run on main (run main's suite first if unsure;
pre-existing SSR TextEncoder + use-velocity failures don't count).
Branch feature/transition-out-v12; PR body links #1725, #2636, and credits
PR #2951 as the origin. Note gh pr edit is broken on this repo — if edits
are needed use gh api -X PATCH repos/motiondivision/motion/pulls/<n> -f body=....
Comment the shipped API on #1725 and close as completed; cross-comment on
#2636 (see plans/issues/issue-2636.md, which is gated on this plan).
In scope: the v12 equivalents of the 5 branch files —
packages/motion-dom/src/animation/types.ts,
packages/motion-dom/src/value/index.ts,
packages/motion-dom/src/animation/interfaces/visual-element-target.ts,
packages/framer-motion/src/motion/__tests__/transition-out.test.tsx (create),
dev/react/src/examples/Animation-transition-out.tsx (create).
Out of scope: transitionFrom/transitionTo map syntax from the thread
(explicitly superseded by the maintainer's choice of out); animation-state
internals (packages/motion-dom/src/render/utils/animation-state.ts) — the
branch deliberately implements this at the value level, not the variant
resolver.
yarn build exit 0; client suite has no new failuresplans/issues/README.md row updatedvalue.nextTransition conflicts with v12 MotionValue internals (e.g. a
same-named field appeared, or liteClient/effects paths bypass
animateTarget) → STOP and report.out is not set (regression sweep red) → STOP.nextTransition is consumed by the next variant animation per value; the
effects/VisualElement unification (branch worktree-style-effect) must
preserve this handoff when animateTarget is reshaped — flag in PR.transition.out at release time.