framer / motion

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

[FEATURE] Flag to instantly finish animations #1160

Closed evanpurkhiser closed 5 months ago

evanpurkhiser commented 3 years ago

Is your feature request related to a problem? Please describe.

Hi all. We over at @getsentry use framer-motion (it's amazing!). We also do visual regression testing on our PRs, which means we generate screenshots from selenium based acceptance tests, then visually compare the screenshots against the screenshots generated from master.

For the screenshot comparisons, we have to make sure all animations are disabled. For framer motion We've added a testableTransition utility, which we then wrap all of our transition values with. The idea is that it just sets the transition type to false essentially disabling the animation.

This has generally worked, but we've noticed some places where the screenshots produce minorly different results between runs. You can see one here https://storage.googleapis.com/sentry-visual-snapshots/getsentry/sentry/6ad776199421d49c927f23c0a62877b96ad348e5/index.html. Maybe it is related to inconsistencies in how the browser renders elements on a gpu accelerated layer?

I was wondering if there was any better way to globally "disable" animations in framer motion.

Describe the solution you'd like MotionConfig could have an option to force framer motion to never "animate" anything, and always immediately transition states. This would be nicer than having to remember to always use testableTransition.

Open to any ideas. Thanks for this incredible library!

lewisl9029 commented 2 years ago

I have a really crude temporary workaround for this involving using a combination of <MotionConfig reducedMotion="always"> to disable all transitions involving movement, and a * { opacity: 1 !important; } global style to disable opacity transitions, and so far that seems to have disabled everything I needed.

Though the obvious severe caveat here is that this means opacity styles won't show up in tests, without tacking on another layer of !important (which would break opacity transitions in non-test environments).

I'd be happy to submit a PR to implement this properly if the maintainers (cc @mattgperry?) are open to accepting it! I'm thinking the API and implementation could be analogous to reducedMotion in MotionConfig, except just a disable boolean prop that disables all transitions, not just ones that cause movement.

mattgperry commented 2 years ago

We would quite like something like this in Framer too so I might be able to take a look in the next couple

jzecca commented 1 year ago

Very interested in this.

I need to generate screenshots of my app using Puppeteer, and being able to skip/disable all animations globally would be awesome.

gajus commented 1 year ago

Does anyone have workarounds to make this work?

gajus commented 1 year ago

Our solution:

import {
  type ForwardRefComponent,
  type HTMLMotionProps,
  motion as Motion,
} from 'framer-motion';
import { forwardRef } from 'react';

const ReducedMotionDiv: ForwardRefComponent<
  HTMLDivElement,
  HTMLMotionProps<'div'>
> = forwardRef((props, ref) => {
  const newProps = {
    ...props,
    animate: undefined,
    initial: undefined,
    transition: undefined,
    variants: undefined,
    whileDrag: undefined,
    whileFocus: undefined,
    whileHover: undefined,
    whileInView: undefined,
    whileTap: undefined,
  };

  return <Motion.div {...newProps} ref={ref} />;
});

export const motion = new Proxy(Motion, {
  get: (target, key) => {
    if (key === 'div') {
      return ReducedMotionDiv;
    }

    // @ts-expect-error - This is a proxy, so we can't be sure what the key is.
    return target[key];
  },
});

export {
  AnimatePresence,
  type Variants,
  type HTMLMotionProps,
  type MotionProps,
  type TargetAndTransition,
  type Transition,
  type Spring,
} from 'framer-motion';
adam-thomas-privitar commented 1 year ago

Yeh we really need this. I appreciate the attempt at sensible accessibility defaults when always or user is used -- but this options is also famously useful for testing, but only when it completely disables animations. MotionConfig needs a new prop like reducedMotionMode that can be set to all-animations or something.

stephanbogner commented 1 year ago

I'd also appreciate this option.

FYI: My case is not regarding testing, but to give users the option to turn off transitions completely in case their hardware isn't capable enough (also some people seem to just hate transitions).

gajus commented 1 year ago

@mattgperry Is this still being worked on by you, would accept contributions, – how can we make this happen?

rhys-e commented 1 year ago

For the purposes of disabling animations for visual regression tests, this worked for me in React. Perhaps it'll be useful for someone:

AnimationContext.ts that can be injected:

import { createContext } from "react";

const AnimationContext = createContext({
  disableAnimations: false,
});

export default AnimationContext;

HOC wrapper for motion-div. This uses a normal div when disableAnimations is set to true MotionDiv.ts:

import React, { useContext } from "react";
import { motion, HTMLMotionProps } from "framer-motion";
import AnimationContext from "./AnimationContext";

type MotionDivProps = React.PropsWithChildren<HTMLMotionProps<"div">>;
type DivProps = React.PropsWithChildren<React.HTMLAttributes<HTMLDivElement>>;

const MotionDiv: React.FC<MotionDivProps> = ({ children, ...props }) => {
  const { disableAnimations } = useContext(AnimationContext);

  if (disableAnimations) {
    return <div {...(props as unknown as DivProps)}>{children}</div>;
  }

  return <motion.div {...props}>{children}</motion.div>;
};

export default MotionDiv;

At the root of the app (in my case, in nextjs _app.ts), wrap the tree with the context provider:

import AnimationContext from "../AnimationContext";

// or however you'd like to inject the value
const disableAnimations = !!process.env.NEXT_PUBLIC_DISABLE_ANIMATIONS;

<AnimationContext.Provider value={{ disableAnimations }}>
   <MyApp />
</AnimationContext.Provider>

Now, in my components instead of import motion-div I use MotionDiv:

import MotionDiv from "./MotionDiv";

<MotionDiv
          className="absolute -z-10 h-full w-full"
          initial={{ scale: "95%" }}
          animate={{ scale: "100%" }}
          transition={{ duration: 3, ease: "circOut" }}
        >
....
</MotionDiv>
jlarmstrongiv commented 1 year ago

This flag would also be great to support accessibility for users who prefers-reduced-motion

SaadBazaz commented 1 year ago

I'm surprised this hasn't been implemented yet. Given that accessibility, automated testing, dev mode (developers hate having to see animations for everything they test, it just gets in the way.) exist.

sonnyt commented 9 months ago

We use next.js server rendering. We resolved it by creating a wrapper around the motion component. It falls back to plain elements if motion is disabled. We also applied the whileInView styles to the element to replicate the "end" state of the animation. It is a simple and naive solution, but it works for us.

import {createElement} from 'react';
import {motion} from 'framer-motion';

export default new Proxy(
  {},
  {
    get: (_, tagName) => {
      return ({children, ...props}) => {
        if (process.env.DISABLE_MOTION) {
          if (!props.style) {
            props.style = {};
          }

          if (props.whileInView) {
            props.style = Object.assign(props.style, props.whileInView);
          }

          return createElement(tagName, props, children);
        }

        return createElement(motion[tagName], props, children);
      };
    },
  }
);
PCPbiscuit commented 6 months ago

Are you guys accepting PRs related to this? It's a very important and helpful feature. People have to come up with weird workarounds especially in SSR.

vsnthdev commented 5 months ago

After almost 2 and half years, it's finally there 🙌

mattgperry commented 5 months ago

In 10.17.0

import { MotionGlobalConfig } from "framer-motion"

MotionGlobalConfig.skipAnimations = true