Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Shared layout animations not working when rendered in React portal #1524

Closed
ciampo opened this issue Apr 25, 2022 · 11 comments
Closed
Labels
bug Something isn't working

Comments

@ciampo
Copy link

ciampo commented Apr 25, 2022

1. Read the FAQs 👇

2. Describe the bug

Shared layout animations don't animate when:

  • rendered inside a React portal, AND
  • there is an existing motion node that would be normally the parent node of the shared layout animation, but it's not anymore after the animation component gets "teleported" through the portal

Basically the combination of these two factors breaks the shared layout animation — instead, only the "first" and "last" states of the animation are rendered (without interpolation).

Initially reported in WordPress/gutenberg#40276

3. IMPORTANT: Provide a CodeSandbox reproduction of the bug

A CodeSandbox minimal reproduction will allow us to quickly follow the reproduction steps. Without one, this bug report won't be accepted.

https://codesandbox.io/s/framer-motion-and-portals-ozvlh3

4. Steps to reproduce

Steps to reproduce the behavior:

  1. Create a component that internally uses shared layout animations via the layoutId prop
  2. Render the component as follows:
// pseudo-code. for a real case example, visit the CodeSandbox link above
<motion.div>
  <PortalBoundary>
    <ComponentThatUsesSharedLayoutAnymations />
  </PortalBoundary>
</motion.div>
  1. Notice how the shared layout animation doesn't animated (it goes from initial to final state of the "animation" without interpolating)
  2. Swap the motion.div wrapper elements with a simple div
  3. The animation works as expected

After some time spent investigating, I believe that the ProjectionNode associated to the element participating in the shared layout animation keeps referencing the wrapper motion.div as its root node, even after the component gets rendered in another part of the dom (via the portal). This could result in buggy behaviour, especially given how ProjectionNodes forward updates via their root nodes.

5. Expected behavior

The shared layout animation works as expected, interpolating smoothly between the initial and final state of the animation.

6. Video or screenshots

framer-motion-portal-bug.mp4

7. Environment details

I don't think it's relevant, but I can reproduce the bug on Mac OS Chrome, Mac OS Firefox, Mac OS Safari (all latest versions).

@ciampo
Copy link
Author

ciampo commented May 2, 2022

Hey @mattgperry 👋 Any chance you (or someone else collaborating on this project) are going to be able to take a look at this soon-ish ?

Thank you!

@ciampo
Copy link
Author

ciampo commented Aug 8, 2022

Hey @mattgperry , I would appreciate it if you (or any other contributors of this project) could reply with some feedback about this bug report — is it an actual bug? Do you know of a workaround? Are you planning on working on a fix, or can you at least give any pointers to what a fix could look like? I'm also happy to help to open a PR, with the right help and collaboration.

Thank you

@tob
Copy link

tob commented Sep 6, 2022

We are experiencing the same bug when trying to use animations inside a modal rendered via portal.

@birmitt
Copy link

birmitt commented Oct 12, 2022

I have experienced another issue of shared element animation, when the parent motion node has set a translation via the x or y style prop. I assume that this is caused by the same bug, described above.

Without a parent motion node – animation works fine

This setup has no parent motion node. The animation works great and the menu button morphs into the menu.
Both nodes share the same layout id.

Without motion parent

// Pseudo code to show the basic setup
<div>
  <MenuButton layoutId="menu"/>
  <Portal>
    <Menu layoutId="menu"/>
  </Portal>
</div>

With a parent motion node – animation broken

Now the setup with a motion node as a parent where x is set to 100.

When the menu is opened, the shared element animation completes immediately. This is the side-effect described by @ciampo. It happens always, with or without x.

The second side-effect occurs when the menu re-renders, e.g. because of the menu item highlighting or content changes. It causes the position animation to start again. This side-effect only occurs, when x is set.
In addition one can see that the start coordinate is still at x = 0, not at 100 as it should be.

With motion parent

Comparing to the working setup, the first level div becomes a motion.div.

// Pseudo code to show the basic setup
<motion.div style={{x: 100}}>
  <MenuButton layoutId="menu"/>
  <Portal>
    <Menu layoutId="menu"/>
  </Portal>
</motion.div>

I can reproduce the bug on Mac OS with Chrome Firefox and Safari (all latest versions) with framer-motion at 7.5.3.

@birmitt
Copy link

birmitt commented Oct 17, 2022

An easy fix, at least in my case, is to reset the MotionContext.
As I understand this context is created by any motion node. Among other information, it propagates the animation variants downstream. My menu is mounted in a portal so I don't need this feature.

// Pseudo code to show the basic setup
<motion.div style={{x: 100}}>
  <MotionContext.Provider value={{}}>
    <MenuButton layoutId="menu"/>
    <Portal>
      <Menu layoutId="menu"/>
    </Portal>
  </MotionContext.Provider>
</motion.div>

The MotionContext created by the motion.div gets directly overwritten with an empty one. Now there is no connection between motion.div and Menu anymore. The shared element animation works perfectly.

This solution may has other implications. I haven't noticed any side-effects so far.
For me it generally sounds logical to disconnect a parent motion node from a portaled motion node because they are not in the same visual tree.

@ciampo
Copy link
Author

ciampo commented Oct 17, 2022

Hey @birmitt , I tried to apply your tentative fix to a clone of the CodeSandbox link that I posted above, and the animations worked again!

I'll try to test this change a bit more in depth in the next days, by applying it to this PR I was previously working on.

@Zombobot1
Copy link

I have the same problem. I have to avoid usage of layout animations in portals.

@lucasvieceli
Copy link

lucasvieceli commented Dec 3, 2022

Eu tenho o mesmo problema. Tenho que evitar o uso de animações de layout em portais.

the problem is when you need to make a modal kkkk

@ciampo
Copy link
Author

ciampo commented Jun 8, 2023

As @birmitt mentioned, the problem seems to be indeed related to the motion context: that is because, when using react portals, the React tree and the DOM tree don't overlap. In particular, the contents of the portal would normally access the motion context of the react parent component, while they would instead need to access the motion context of where the portal is rendered (ie. the "slot").

I managed to fix this issue by reading the motion context around the "slot", passing it to the "fill", and recreating a new motion context with that same value — basically, manually "forwarding" the value of the motion context.

Hope this can be of help to other folks. Feel free to reopen this issue in case that doesn't work for you

@ciampo ciampo closed this as completed Jun 8, 2023
@lucasvieceli
Copy link

@ciampo Do you have an example of how you fixed it?

@ciampo
Copy link
Author

ciampo commented Sep 1, 2023

@ciampo Do you have an example of how you fixed it?

You can take a look at these 3 commits, part of a larger PR — although I appreciate that without much context on the project, it may be a bit overwhelming.

Basically the idea is to forward the motion context through the react portal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

5 participants