framer / motion

Open source, production-ready animation and gesture library for React
https://framer.com/motion
MIT License
23.75k stars 807 forks source link

[BUG] Layout animations do not animate completely when container is scrolled #1471

Open bhollis opened 2 years ago

bhollis commented 2 years ago

1. Describe the bug

I have a scrollable div, within which I have a list of motion.divs. These use AnimatePresence to animate themselves away, and each has layout set so the remaining items will animate to fill the hole left by that item.

When the div is scrolled, removing an item results in incomplete transitions and overlapping items, or other items above the removed items will move down, e.g.:

Screen Shot 2022-02-28 at 5 53 22 PM

3. IMPORTANT: Provide a CodeSandbox reproduction of the bug

https://codesandbox.io/s/framer-motion-layout-animation-bug-when-scrolled-pot7y3?file=/src/styles.css

4. Steps to reproduce

  1. Scroll the items in the inner div
  2. Then click an item to remove it.

Sometimes, they will overlap or leave a gap. This minimal repro renders lighter-weight components than my real app, so it can be a bit harder to see it mess up - try scrolling with a trackpad and clicking before the scroll fully settles (though in my full application, this happens reliably even when scrolling has settled).

5. Expected behavior

I expect the item that's clicked to animate out, and the remaining items to animate into their place in the list.

6. Video or screenshots

Screen Shot 2022-02-28 at 5 59 03 PM

https://user-images.githubusercontent.com/313208/156072905-5884d634-02aa-40e2-b1a3-2c7cc49e9712.mov

7. Environment details

Chrome 98 on an M1Max MacBook Pro.

mattgperry commented 2 years ago

Does this persist if you convert the overflow: scroll to a motion.div and add the layoutScroll prop?

fyfirman commented 2 years ago

Hi, i have same issue here. I have tried @mattgperry suggestion to change overflow: scroll to layoutScroll but it's still occured. On my app, this bug only occur if i remove the middle of item, so if i remove first and last item it will be fine. Then, i try to inspect what happen on the css, i found the transform is not going back to none.

Hope this help.

https://user-images.githubusercontent.com/37064641/158639254-009b4cde-d199-4c55-af15-02c8ec9928d1.mp4

bhollis commented 2 years ago

layoutScroll does seem to fix it in my sandbox. I hadn't seen that property before :-D. I'll apply it in my full app and see if it helps.

paragkatoch commented 2 years ago

any update on how to fix this?

adred commented 2 years ago

layoutScroll doesn't fix the issue for me.

mghdmi commented 2 years ago

converting the container to motion.div and add overflow: scroll with layoutScroll prop fix it for me too

RannyArcher commented 2 years ago

i think you should use usePresence hook, and call safeToRemove function which is returned with isPresent which indicated the presence of element as boolean (doc: https://www.framer.com/docs/animate-presence/#usepresence) i am not sure will this work in your case or no, i wish it help you.

ARiyou2000 commented 11 months ago

None of these worked for me:

With Screen as the container:

Screencast from 11-14-2023 11:12:09 AM.webm

With scrollable container:

Screencast from 11-14-2023 11:13:57 AM.webm

Code:

"use client";

import { motion, useScroll } from "framer-motion";
import { useEffect, useRef } from "react";

const Card = () => {
  const containerRef = useRef(null);
  const { scrollYProgress } = useScroll({
    target: containerRef,
    offset: ["0 1", "1.33 1"],
  });

  useEffect(() => {
    console.log(scrollYProgress);
  }, [scrollYProgress.get()]);

  return (
    <motion.div
      ref={containerRef}
      layout
      style={{
        scale: scrollYProgress,
        // opacity: scrollYProgress,
      }}>
      <div className="flex items-center justify-center p-20 w-full bg-slate-500">
        AAA
      </div>
    </motion.div>
  );
};

////////////////////////////////////////////////////////////////////////////////
export default function Home() {
  const ref = useRef(null);
  const { scrollYProgress } = useScroll({ container: ref });

  return (
    <div dir={"ltr"}>
      <svg id="progress" width="100" height="100" viewBox="0 0 100 100">
        <circle cx="50" cy="50" r="30" pathLength="1" className="bg" />
        <motion.circle
          cx="50"
          cy="50"
          r="30"
          pathLength="1"
          className="indicator"
          style={{ pathLength: scrollYProgress }}
        />
      </svg>
      <motion.ul layoutScroll={true} style={{ overflow: "scroll" }} ref={ref}>
        <Card />
        <Card />
        <Card />
        <Card />
        <Card />
        <Card />
        <Card />
        <Card />
        <Card />
        <Card />
      </motion.ul>
    </div>
  );
}

it seems elements are still taking the screen as their container at the initial phase (Since the fifth element scale corresponds to what it should be with the screen as its container).