Skip to content

Fix AnimatePresence popLayout RTL positioning#3653

Open
mattgperry wants to merge 1 commit intomainfrom
worktree-fix-issue-2952
Open

Fix AnimatePresence popLayout RTL positioning#3653
mattgperry wants to merge 1 commit intomainfrom
worktree-fix-issue-2952

Conversation

@mattgperry
Copy link
Collaborator

Summary

  • Bug: AnimatePresence with mode="popLayout" used the physical left CSS property to position exiting elements. In RTL layouts with fit-content containers, the container shrinks from the left edge (right edge stays fixed), causing the absolutely positioned exiting element to visually shift.
  • Cause: PopChild.tsx hardcoded left: ${offsetLeft}px regardless of text direction. In RTL, when the container shrinks, its left edge moves right, dragging the left-anchored element with it.
  • Fix: Detect the element's computed direction during measurement and use the appropriate physical CSS property — right in RTL (anchoring to the stable edge) or left in LTR (default behavior unchanged).

Fixes #2952

Test plan

  • New Cypress E2E test (animate-presence-pop-rtl) verifies exiting element maintains its viewport position in an RTL flex container with fit-content
  • Test passes on React 18 and React 19
  • All 6 existing animate-presence-pop Cypress tests still pass
  • All 776 unit tests pass (10 skipped — pre-existing)
  • Build succeeds

🤖 Generated with Claude Code

PopChild used the physical `left` CSS property for positioning exiting
elements, which breaks in RTL layouts where fit-content containers
shrink from the left. Now detects text direction and uses the
appropriate physical property (`right` in RTL) to anchor to the stable
edge.

Fixes #2952

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR fixes a visual positioning bug in AnimatePresence with mode="popLayout" where exiting elements would shift horizontally in RTL layouts with fit-content containers. The root cause was that PopChild.tsx always used the physical left CSS property to anchor the popping element, but in RTL fit-content containers the right edge is the stable one — the left edge migrates when the container shrinks around remaining children.

Key changes:

  • PopChild.tsx now reads computedStyle.direction during the getSnapshotBeforeUpdate measurement snapshot and stores it alongside the element's dimensions.
  • In useInsertionEffect, when direction === "rtl" and anchorX === "left" (the default), the injected style uses right: ${right}px instead of left: ${left}px, anchoring to the stable right edge.
  • A new Cypress E2E test (animate-presence-pop-rtl) and dev fixture verify the fix with a two-element RTL flex container that has fit-content width.

Note: The fix correctly resolves the default anchorX="left" case. However, for the less-common anchorX="right" explicit override in RTL contexts, the swap has the opposite effect — the old code coincidentally used the stable right edge (right: ${right}) for the anchorX !== "left" branch, while the new code switches that to left: ${left} (the unstable edge in RTL fit-content). This edge case may warrant a follow-up or additional documentation.

Confidence Score: 4/5

  • Safe to merge — fixes a real RTL bug with appropriate test coverage; one narrow edge case with explicit anchorX="right" in RTL is worth a follow-up.
  • The core fix is mathematically sound: size.right = parentWidth - width - offsetLeft correctly captures the physical right offset, and using right: ${right}px anchors the element to the stable edge in RTL fit-content containers. The default path (no anchorX → defaults to "left") is fully covered by the new Cypress test. The minor concern is that the logic swap also inadvertently changes behaviour for the explicit anchorX="right" in RTL, potentially reintroducing a positional shift in that narrow scenario. All existing tests pass.
  • Pay close attention to packages/framer-motion/src/components/AnimatePresence/PopChild.tsx lines 110-112 — the swapped RTL anchor logic for anchorX="right" cases.

Important Files Changed

Filename Overview
packages/framer-motion/src/components/AnimatePresence/PopChild.tsx Adds RTL direction detection via computedStyle.direction and swaps the CSS anchor property for exiting elements. The fix correctly addresses the default anchorX="left" case, but inverting the anchorX="right" path for RTL may re-introduce the same positioning issue for that explicit override.
packages/framer-motion/cypress/integration/animate-presence-pop-rtl.ts New Cypress E2E test that verifies the exiting element maintains its viewport left position after the RTL fit-content container shrinks. Covers the primary bug scenario adequately; does not test anchorX="right" in RTL.
dev/react/src/tests/animate-presence-pop-rtl.tsx New dev test fixture for the RTL popLayout scenario. Sets up a dir="rtl" outer wrapper with a fit-content container and two motion elements, correctly representing the reported bug.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[getSnapshotBeforeUpdate\nElement going from present → absent] --> B[Read computedStyle.direction\nStore in size.direction]
    B --> C[useInsertionEffect fires\nElement is no longer present]
    C --> D{isRTL?\ndirection === 'rtl'}
    D -- No / LTR --> E{anchorX === 'left'?}
    D -- Yes / RTL --> F{anchorX === 'left'?}
    E -- Yes --> G["left: left_px\n(stable left edge in LTR)"]
    E -- No --> H["right: right_px\n(right-anchored in LTR)"]
    F -- Yes / Default --> I["right: right_px\n✅ Stable right edge in RTL fit-content"]
    F -- No / anchorX='right' --> J["left: left_px\n⚠️ Unstable left edge in RTL fit-content"]
    G --> K[Inject CSS rule via style sheet]
    H --> K
    I --> K
    J --> K
    K --> L[Exiting element held in place during animation]
Loading

Last reviewed commit: 3212dd9

Comment on lines +110 to +112
const x = anchorX === "left"
? (isRTL ? `right: ${right}` : `left: ${left}`)
: (isRTL ? `left: ${left}` : `right: ${right}`)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 RTL positioning for anchorX="right" may still shift

When anchorX is explicitly set to "right" in an RTL fit-content container, the fix now uses left: ${left} (physical left) which is the unstable edge in RTL. In a right-to-left fit-content container, the right edge remains fixed when the container shrinks — so both anchorX="left" and anchorX="right" would need to use right: ${right} to stay visually anchored.

The new behavior:

// anchorX === "right", isRTL === true
`left: ${left}` // left edge is unstable in RTL fit-content → element shifts

The pre-fix behavior for anchorX="right" in RTL happened to be correct:

// anchorX !== "left" → `right: ${right}` (stable edge in RTL fit-content) ✓

This PR only fixes the default anchorX="left" case. If anchorX="right" is used in RTL, the same positional shifting bug can still occur. Consider whether the swap should always use the stable edge (right) in RTL regardless of anchorX, or document that anchorX="right" is unsupported in RTL contexts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] AnimatePresence mode="popLayout" assumes LTR, works differently/unexpectedly in RTL

1 participant