plans/issues/issue-2656.md
Executor instructions: Follow step by step; run the drift check first. Update the status row for this plan in
plans/issues/README.mdwhen done.Drift check (run first):
gh api repos/motiondivision/motion/issues/2656 --jq .state→ expect"open". If already closed, mark this plan DONE and stop. Then confirm the excerpts below still matchpackages/motion-dom/src/utils/mix/visibility.tsandpackages/framer-motion/src/motion/__tests__/animate-prop.test.tsx.
42bfbe3ed, 2026-06-11Reported 2024-05-10 against 11.0.11: variants animating display: block ↔
display: none (alongside opacity) stopped returning to none when hiding.
Worked in 11.0.10. Root cause: 11.0.11's async-keyframe-resolution rewrite
(CHANGELOG.md:2111-2118, "Keyframes now resolved asynchronously") changed how
discrete string keyframes were mixed, losing the none end state.
mattgperry confirmed the regression on 2024-05-13 ("this did get inadvertently
broken though the previous behaviour wasn't great either — instantly animating
to 'none'") and landed the fix the same day: commit 9dc6e6aa1 ("Updating",
2024-05-13) added mixVisibility, released in v11.2.0 (CHANGELOG.md:1932,
entry at line 1936: "Binary visibility interpolation i.e display: ["block", "none"] now maintains the visible state throughout the animation"). The issue
was never closed. The 2024-08-26 "Any progress?" comment predates wide adoption
of the fix only in the commenter's project — the fix was already released.
packages/motion-dom/src/utils/mix/visibility.ts:8-14 — hiding holds the
visible keyframe until the end; showing applies the visible keyframe
immediately:
export function mixVisibility(origin: string, target: string) {
if (invisibleValues.has(origin)) {
return (p: number) => (p <= 0 ? origin : target)
} else {
return (p: number) => (p >= 1 ? target : origin)
}
}
packages/motion-dom/src/utils/mix/complex.ts:112-120 — mixComplex routes
display/visibility-style keyframes (invisibleValues = "none",
"hidden") into mixVisibility.packages/motion-dom/src/animation/utils/can-animate.ts:36 —
if (name === "display" || name === "visibility") return true keeps these
values on the JS animation path (display is not in acceleratedValues, so
WAAPI is never used for it).JSAnimation.tick writes the real final keyframe:
packages/motion-dom/src/animation/JSAnimation.ts:335-342
(state.value = getFinalKeyframe(...) → "none").packages/framer-motion/src/motion/__tests__/animate-prop.test.tsx:250-307
("animate display none => block immediately switches to block", "animate
display block => none switches to none on animation end").dev/react/src/examples/Animation-display-visibility.tsx
(added by the fix commit).npx jest --config packages/framer-motion/jest.config.json --testPathPattern="animate-prop" -t "display"
Verify: all display tests pass (≥3 tests, 0 failures). If any fail, STOP — that's a live regression; this plan's verdict is wrong and the issue needs a FIX plan instead.
The issue's StackBlitz (vitejs-vite-mc7x2z) is described fully in the issue
body: overlay with variants {opacity: 1, display: "block"} /
{opacity: 0, display: "none"}, toggled by a button; bug = display stayed
block after hiding. If you want browser-level proof, create
dev/react/src/tests/animate-display-none.tsx + a spec asserting
getComputedStyle(el).display === "none" after the hide animation completes
(and "block" mid-animation), and run it per the CLAUDE.md Cypress recipe on
React 18 and 19. Delete nothing — keep the test as permanent coverage if you
write it. This step may be skipped; Step 1 is the gate.
Open plans/issues/README.md and find the row for issue-2656. If not marked
APPROVED, set this plan's row to BLOCKED and stop.
gh api repos/motiondivision/motion/issues/2656/comments -f body="This was fixed in v11.2.0 (released 2024-05-14, four days after this report): binary visibility interpolation now keeps the element visible throughout the animation and applies display: none only when the hide animation completes (and conversely applies the visible value immediately when showing). The behaviour is covered by unit tests (animate-prop.test.tsx). If you can still reproduce on motion@12, please open a new issue with a reproduction."
gh api -X PATCH repos/motiondivision/motion/issues/2656 -f state=closed -f state_reason=completed
Verify: gh api repos/motiondivision/motion/issues/2656 --jq .state → "closed".
completed (only after APPROVED)plans/issues/README.md status row updatedplans/issues/issue-2563.md) is the same root cause via
useAnimate — close it together with this one.