Svelte transition between two elements "jumps"
Asked Answered
E

3

22

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>

EDIT: Non-functional CodeSandbox links originally in this post and referenced in the comments have been removed.

Entablature answered 23/1, 2020 at 15:37 Comment(2)
I've written a blog post about solving this problem with CSS grid. hope it helps!Foothill
@KyleMit -- unfortunately, no. I've edited my question to indicate that, so as not to waste anyone's clicks. I did do a small reproduction and solution CodePen for you, though. The jumping issue can be seen here; here is my usual fix these days. The big assist comes form "onoutrostart," and you can use the event target to capture and manipulate the HTML element's positioning as needed. You can also, as I suggested, do the same in a custom transition.Entablature
G
25

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!

Gala answered 24/1, 2020 at 8:35 Comment(8)
Thanks so much! Good to know my method wasn't more intensive than it needed to be, and nice to see some alternatives. Setting absolute height on the container is tough, because I never know how high exactly my content will be (without JS). Vue has a "move" transition that will smoothly move other items out of the way of differently-sized content--would be nice to see these things in Svelte!Entablature
@JacobRunge For finding the exact height of an element you can always use bind:this to get the HTML element into a variable then get the offsetHeight from the element in onMount like this: svelte.dev/repl/aaa5744fb6c64c5dadf707941c9e45c1?version=3.18.0 But it does cause some extra work for something that Vue takes care of out of the box. Same with the move transition, Svelte can do something similar with animate, crossfade, and flip, but the examples given are complex. It would be nice to see more abstraction on animations like Vue does.Gala
Something to note: If the content has different sizes, it can change the size of your grid and cause a different kind of "jumping." To fix that, you need to keep the grid from shrinking/growing.Ostiary
it also does not work with transition:fly={{ y: 150 }}Slouch
@Slouch What does not work with that transition?Gala
@Gala it is bound to parent container height. I made a REPL, you can clearly see the difference: svelte.dev/repl/2bd362ec6dcc492bad2088adda799735?version=4.2.7Slouch
@Slouch if you mean the overflow of the containing element being hidden you just remove that in the CSS. It only matters for transitions on the X axis to prevent the window getting a horizontal scrollbar, here's that same REPL with overflow removed svelte.dev/repl/b97afd30625a41e79f315864616173eaGala
@Gala oh you are right, my badSlouch
L
10

This is how the basic setup of the “grid way” looks like:

<script>
    import { scale } from "svelte/transition"
  
    let condi = true;
</script>

<div class="container">
    {#if condi}
        <div class="item" in:scale out:scale />
    {:else}
        <div class="item" in:scale out:scale />
    {/if}
</div>

<style>
    .container {
        display: grid;
    }

    .item {
        grid-column-start: 1;
        grid-column-end: 2;
        grid-row-start: 1;
        grid-row-end: 2;
    }
</style>

Svelte REPL

Lederhosen answered 19/9, 2022 at 10:7 Comment(1)
If you're using tailwind: "col-span-full row-span-full" as classes for both items also works.Lashanda
S
7

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.

Souza answered 6/12, 2020 at 19:0 Comment(3)
thanks for contributing! It would probably be good to include some of the REPL example code that you've linked to in your answer. The problem with link-based answers is that if the link no longer works, the answer loses meaning.Isogamy
Thanks for the tip! Hope the update provides a good explanation.Souza
Or the pragmatic version: let outroDone = false; ... {#if someConditionToShowFirstNode} <div transition:fly|local="..." on:outroend={()=>{outroDone=true}}></div> {:else if outroDone} <div transition:fly|local="{{ ... no delay needed ... }}"></div> {/if}Blanc

© 2022 - 2024 — McMap. All rights reserved.