Open mattgperry opened 2 years ago
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.
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: {}
}}
...
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
syntaxHere I would like to note the few obvious syntaxes I've thought of declaring transitions separately from variants.
<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.
<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.
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.
I assume you wouldn't want to surface such a helper function from the API, though. Maybe it could be added to the docs.
It would be necessary to take a look at precedence in these syntaxes.
Non-wildcard relations should likely always override wildcard relations.
Eg.: variant1 -> variant2
would take precedence over variant1 -> *
and * -> variant2
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.:
variant1 -> *
and * -> variant2
, variant1 -> *
might be chosen as it is specified first.variant1 -> *
and * -> variant2
, * -> variant2
might be chosen as it is specified last.variant1 -> *
and * -> variant2
, variant1 -> *
might be chosen as a variant is specified for the source.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
.Again, please take this with a grain of salt and have a great day 😊
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.
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 👏
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
There's a bit of a blind spot in the API at the moment where if you take
For instance you can see that when we re-enter the
animate
state there'll be adelay
applied. When really thedelay
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
?Or