I love Svelte, but I'm stuck on something basic (though merely cosmetic). The following code should transition between two elements smoothly, but instead it "jumps"--apparently making room for the incoming element before it arrives.
The problem is similar to this one that Rich Harris noted a few years back, but I don't see that a solution was implemented. All examples on the Svelte tutorial site transition only a single element.
Here is the basic markup/code:
{#if div1}
<div
in:fly={{ x: 100, duration: 400, delay: 400 }}
out:fly={{ x: 100, duration: 400 }}>Div 1</div>
{:else}
<div
in:fly={{ x: 100, duration: 400, delay: 400 }}
out:fly={{ x: 100, duration: 400 }}>Div 2</div>
{/if}
<button on:click={()=>{ div1 = !div1}}>Switch</button>
A working equivalent in Vue would be:
<transition name="fly" mode="out-in">
<div v-if="div1">Div 1</div>
<div v-else>Div 2</div>
</transition>
Here is a Code Sandbox example. You can see the button jump down to make room for the new element. I added an "in" transition delay equal to the duration of 400 (I know that's default, but I wanted to set it explicitly for clarity's sake). The delay should allow the first element to transition out before transitioning the next one in, as noted in the first link (what Harris called "hacky use of delay") and suggested here.
I also tried explicitly setting the element being outro'd to position: absolute so that it wouldn't take up space. Here is a (still not working properly) example. Seems a little inelegant, even if it were working. For some reason, the transition is overriding setting a class that sets position:absolute.
Any help is greatly appreciated!
UPDATE: I got the desired effect with this code. What I did here was copy and modify Svelte's fly transition to take an additional parameter--'position,' which can be set to 'absolute' or 'relative' or whatever you want. A few tweaks to the CSS to ensure there are no strange side-effects (a container set to position: relative, and setting width of each element to 100% to ensure they don't change size). This works, but I still feel there should be a less labor-intensive method, without modifying Svelte's transitions.
I came over from Vue as well, the out-in is one thing I miss with Svelte. Rich Harris even acknowledged it prior to Svelte 3 but never really implemented a fix as far as I'm aware.
The problem with the single condition, delay-only, out-in transition method is that Svelte is creating the incoming element once the condition switches despite the delay on the in transition. You can slow the transitions way down and check dev tools to see this, both elements will exist the incoming transition delay does not prevent the element from having a size, just visibility.
One way around it is to do what you've done with absolute position, kinda intensive and becomes boilerplate. Another method is to set an absolute height for the container holding the elements being transitioned, pull everything else out of the container (the button in your example) and hide the overflow as seen here, very css dependent and does not always play well with certain layouts.
The last way I've used is a bit more round about but since Svelte has an outroend event that is dispatched when the animation is done you can add a variable for blue or whatever your second condition is and put in an else if block for the second condition (blue here) and wire the trigger so it's checking for the active variable and switching it off, then switch on the other variable inside the outroend event as seen here you can also remove any delay since the duration becomes the delay.
From inspecting the DOM during transitions it seems this is the only way that both elements don't exist at the same time because they depend on separate conditions, I'm sure there are even more elegant ways to achieve this but this works for me.
EDIT:
There is another option available that only works on browsers that support CSS grid spec, luckily that's nearly universal at this point. It's very similar to the absolute positioning method with an added bonus that you don't have to worry about the height of the elements at all
The idea behind this is that with CSS Grid we can force 2 elements to occupy the same space with grid-area
or grid-column
and grid-row
by giving both elements(or more than 2) the same start and end columns and rows on the implicit grid of 1 col by 1 row (grid is smart enough to not create extra columns and rows we won't be using). Since Svelte uses transforms in it's transitions we can have elements coming and going without any layout shift, nice. We no longer have to worry about absolute position affecting elements or about delays, we can fine tune the transition timing to perfection.
Here is a REPL to show a simple setup, and another REPL to show how this can be used to get some pretty sweet layering effects, woah!
If you happen to have more than two states to swap between, abstracting the behavior to a custom store is really helpful. The store could look something like this:
statefulSwap(initialState) {
const state = writable(initialState);
let nextState = initialState;
function transitionTo(newState) {
if(nextState === newState) return;
nextState = newState
state.set(null)
}
function onOutro() {
state.set(nextState)
}
return {
state,
transitionTo,
onOutro
}
}
You can swap between elements using conditional blocks:
{#if $state == "first"}
<h1 transition:fade on:outroend={onOutro}>
First
</h1>
{:else if $state == "second"}
<h1 transition:fade on:outroend={onOutro}>
Second
</h1>
{/if}
This technique emulates out-in
behavior of Vue by initially setting the current state to null
and then applying the new state in onOutro
after the first element has transitioned out.
Here is a REPL example. The advantage here is that you can have as many states as you want with different animation actions and timings without having to keep track of the swap logic. However, this doesn't work if you have a default else
block in your conditional markup.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With