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

[FEATURE] Defining transitions between props #1725

Open
mattgperry opened this issue Oct 4, 2022 · 5 comments · May be fixed by #2951
Open

[FEATURE] Defining transitions between props #1725

mattgperry opened this issue Oct 4, 2022 · 5 comments · May be fixed by #2951
Assignees
Labels
feature New feature or request
Milestone

Comments

@mattgperry
Copy link
Collaborator

mattgperry commented Oct 4, 2022

There's a bit of a blind spot in the API at the moment where if you take

<motion.div
  initial={{ scale: 0 }}
  animate={{ scale: 1, transition: { delay: 1 } }}
  whileHover={{ scale: 2 }}
/>

For instance you can see that when we re-enter the animate state there'll be a delay applied. When really the delay naturally applies to the initial animation.

It'd be good if we could figure out a way to define a transition that applies just on the initial animation. Perhaps there's also value in being able to define specific transitions more generally, like whileHover -> animate?

<motion.div
  initial={{ scale: 0 }}
  animate={{ scale: 1, transition: { delay: 1 } }}
  whileHover={{ scale: 2 }}
  transition={{
    initial -> animate: {},
    whileHover -> animate: {}
  }}
/>

Or

<motion.div
  initial={{ scale: 0 }}
  animate={{
    scale: 1, 
    transitionFrom: {
      initial: { delay: 1 },
      whileHover: { type: "spring" }
    }
  }
  whileHover={{
    scale: 2
  }}
/>
@mattgperry mattgperry added the feature New feature or request label Oct 4, 2022
@mattgperry mattgperry changed the title [FEATURE] initialTransition [FEATURE] Defining transitions between props Oct 4, 2022
@akd-io
Copy link

akd-io commented Oct 4, 2022

I've been thinking about this a bit today and wanted to share in case my thoughts aren't obvious.

Please take this with a grain of salt! I'm by no means an expert here, I'm certain you, Matt, know better than anyone. I also know maintaining OS is hard work. I can't even keep a single project afloat.

I just thought it might be fun to chime in and be a little part of some API design which I find fascinating.

Transitions as edges in a graph

To me, transitions are a phenomenon that inherently appears between states (variants), and therefore I believe it might make sense to model it likewise and declare transitions outside of variant declarations. Think of it like a finite state machine; a graph where states (variants) are nodes and transitions are the edges. In this sense it is arbitrary that the current implementation of transition implies an any variant -> current variant relationship.

In your first example, you provided pseudo code for such syntax, specifically:

...
transition={{
  initial -> animate: {},
  whileHover -> animate: {}
}}
...

Syntactic sugar for variants

Because I'm guessing it can become too verbose to declare the transitions in the previously described manner, I think it makes sense to add syntactic sugar to variants to handle the most frequent transition use cases.

You proposed such syntactic sugar in your second example, specifically:

...
animate={{
  scale: 1, 
  transitionFrom: {
    initial: { delay: 1 },
    whileHover: { type: "spring" }
  }
}
...

With transitionFrom, though, you might also want to add transitionTo. This would provide the following APIs:

  • transition: This prop currently handles any variant -> current variation transitions.
  • transitionFrom: This prop would handle specific variant -> current variant transitions.
  • transitionTo: This prop would handle current variant -> specific variant transitions.

But by the symmetry of these relationships, you might notice that there is no corresponding inverse of transition with the relation current variation -> any variant. As such you might introduce transitionToAny, whose name highlights the fact that transition has really been transitionFromAny in disguise all along.

That would leave us with:

  • transition: Alias of transitionFromAny
  • transitionFrom: specific variant -> current variant
  • transitionFromAny: any variant -> current variant
  • transitionTo: current variant -> specific variant
  • transitionToAny: current variant -> any variant

I would personally just implement a * wildcard to represent any variant, though, and stick to:

  • transition: Alias of transitionFrom with * wildcard.
  • transitionFrom: specific variant -> current variant where specific variant can be a * wildcard.
  • transitionTo: current variant -> specific variant where specific variant can be a * wildcard.

transitions syntax

Here I would like to note the few obvious syntaxes I've thought of declaring transitions separately from variants.

Verbose object notation

<motion.div
  ...
  transitions={
    [
      {
        from: 'variant1',
        to: 'variant2',
        transition: {
          delay: 0.5,
        },
      },
      {
        from: 'variant2',
        to: '*', // `*` acts as a wildcard for any variant.
        transition: {
          delay: 1,
        },
      },
      {
        // Not specifying `from` implicitly sets it to `*`
        to: 'variant3',
        transition: {
          delay: 2,
        },
      },
      {
        from: 'variant4',
        // Not specifying `to` implicitly sets it to `*`
        transition: {
          delay: 4,
        },
      },
    ],
  }
/>

Easy to read and understand, but verbose.

As shown, specifying only one of from and to implies the other is a wildcard.

Concise object notation using arrays

<motion.div
  ...
  transitions={[
    ['variant1', 'variant2', {
      delay: 0.5,
    }],
    ['variant2', '*', {
      delay: 1,
    }],
    ['*', 'variant3', {
      delay: 2,
    }],
  ]}
/>;

Short and fairly easy to read. Unfortunately, Prettier will mess this one up big time if variations aren't written as one-liners. 🙁

You could choose to allow specifying one variant only, and imply it as from or to, but I'd vote against it for readability reasons.

A useful helper function

No matter the syntax, a simple helper function would clean it up a lot:

const makeTransition = (from, to, transition) => ({ from, to, transition });

const MyComponent = () => {
  return (
    <motion.div
      ...
      transitions={[
        makeTransition("hidden", "visible", { duration: 1 }),
        makeTransition("visible", "hidden", {
          duration: 2,
          delay: 10,
          ease: "easeInOut",
        }),
      ]}
    />
  );
};

This helper function plays nicely with Prettier and also makes the code very readable if you are using VS Code's Inlay Hints that show the names of parameters. In any case it will show in the verbosity the user has set in their preferences.
Screenshot 2022-10-04 at 23 10 59

I assume you wouldn't want to surface such a helper function from the API, though. Maybe it could be added to the docs.

Precedence

It would be necessary to take a look at precedence in these syntaxes.

Wildcard vs. non-wildcard

Non-wildcard relations should likely always override wildcard relations.

Eg.: variant1 -> variant2 would take precedence over variant1 -> * and * -> variant2

Wildcard vs. wildcard

A transition that is specified both via a wildcard at the source and via a wildcard at the destination could take into account both the position of the transition in a transitions array and whether the wildcard is on the source or the destination.

Eg.:

  • Given a transitions array with the relations variant1 -> * and * -> variant2, variant1 -> * might be chosen as it is specified first.
  • Given a transitions array with the relations variant1 -> * and * -> variant2, * -> variant2 might be chosen as it is specified last.
  • Given a transitions array with the relations variant1 -> * and * -> variant2, variant1 -> * might be chosen as a variant is specified for the source.
  • Given a transitions array with the relations variant1 -> * and * -> variant2, * -> variant2 might be chosen as a variant is specified for the destination. This is probably better than the former, as this aligns better with the current relationship of transition as mentioned earlier, any variant -> current variant.

Finally

Again, please take this with a grain of salt and have a great day 😊

@mattgperry
Copy link
Collaborator Author

Amazing post! Thanks for help thinking this through.

On transition, transitionFrom etc... I think just these two would be enough. transitionTo and transitionFrom creates a potential conflict and the current paradigm is that the variant being entered is the one that defines the transition used. This is similar to CSS. transitionFrom would be enough to then further designate a specific transition to use for this variant combination.

Ideally we'd add as little API as possible to achieve our aims and I think just this would be enough, vs transitions which could itself get quite verbose.

There's also the added wrinkle of prop names/gestures vs variants.

Prop names, useful with or without variants

<motion.div
  animate={{
    scale: 1,
    transitionFrom: {
      whileHover: { duration: 1 }
    }
  }}
  whileHover={{
    scale: 2
  }}
/>

Or from variant names, only useful with variants

<motion.div
  variants={{
    enter: /** **/,
    exit: {
      transitionFrom: {
        enter: { duration: 1 }
      }
    }
  }}
  whileHover={{
    scale: 2
  }}
/>

Supporting both might be quite difficult though I'll take a look into this as I can see the use of both.

@mattgperry mattgperry added this to the 🎨 Canvas milestone Oct 5, 2022
@mattgperry mattgperry self-assigned this Oct 5, 2022
@akd-io
Copy link

akd-io commented Oct 6, 2022

Makes total sense to try to add as small changes to the API as possible, and you're totally correct it gets more complicated than I outlined when opening the whole transitions can of worms.

transitionFrom would be enough to cover our needs, and I assume other people's.

Would be wonderful to see transitionFrom implemented 👏

@akd-io
Copy link

akd-io commented May 22, 2024

Hey @mattgperry, just hit this problem again, and saw you have been working on this, but closed #2332 a couple weeks ago.

Did you find a blocker that prevents us from implementing transitionForm?

Let me know if not and you think I should give it a shot.

Edit: Is it revived here? #2643

@mattgperry
Copy link
Collaborator Author

Hey @akd-io

This is the closest as I think I'll implement #2951

@mattgperry mattgperry linked a pull request Jan 21, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants