21

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.

2
  • 2
    I've written a blog post about solving this problem with CSS grid. hope it helps!
    – pitamer
    Commented Oct 10, 2021 at 19:21
  • 1
    @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. Commented Jul 10, 2023 at 3:40

3 Answers 3

24

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!

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! Commented Jan 26, 2020 at 16:39
  • 2
    @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.
    – JHeth
    Commented Jan 27, 2020 at 21:50
  • 1
    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.
    – Jehy
    Commented Feb 9, 2022 at 17:58
  • it also does not work with transition:fly={{ y: 150 }}
    – skelaw
    Commented Nov 24, 2023 at 7:39
  • 1
    @skelaw 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/b97afd30625a41e79f315864616173ea
    – JHeth
    Commented Nov 24, 2023 at 18:39
10
+250

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

1
  • 1
    If you're using tailwind: "col-span-full row-span-full" as classes for both items also works. Commented Apr 4 at 9:46
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.

3
  • 2
    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.
    – clayRay
    Commented Dec 7, 2020 at 0:44
  • 2
    Thanks for the tip! Hope the update provides a good explanation. Commented Dec 8, 2020 at 6:08
  • 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}
    – colllin
    Commented Feb 26, 2021 at 1:16

Not the answer you're looking for? Browse other questions tagged or ask your own question.