Yura
Yura

Reputation: 2248

How can I fix this jumping transition?

I'm trying to build some kind of fade slider but got weird jumps between transition, already tried solution from this question but it doesn't work in my case. I can't figure out why this happen, to be clear, please have a look at this gif

visual explanation

If possible, I would like to know which part is causing this as well as how can I fix it. Any kind of help will be very appreciated. Please have a look at my script.. Thanks in advance!

<template>
  <header class="hero">
    <div class="container">
      <div class="textbox">
        <transition name="fade" v-for="(slide, i) in slides" :key="slide">
          <h1 class="punchline" v-if="current == i">{{ slide }}</h1>
        </transition>
        <NuxtLink class="link" to="/">
          <div class="icon-wrapper">
            <Icon icon="chevron-right" />
          </div>
        </NuxtLink>
      </div>
      <div class="indicator">
        <span
          v-for="(slide, i) in slides"
          :key="slide"
          :class="{ active: current == i }"
          class="item"
          @click="animateTo(i)"
        />
      </div>
    </div>
    <Vector :class="color" class="stick-overlay stick-top" file="model-stick" />
    <Vector
      :class="color"
      class="stick-overlay stick-bottom"
      file="model-stick"
    />
  </header>
</template>


<script>
export default {
  data() {
    return {
      current: 0,
      slides: ['Slide 001', 'Slide 002', 'Slide 003']
    }
  },
  computed: {
    color() {
      if (this.current == 1) return 'blue'
      if (this.current == 2) return 'purple'
      return 'default'
    }
  },
  methods: {
    animateTo(index) {
      this.current = index
    }
  }
}
</script>

<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}

.hero {
  @apply flex;
  @apply justify-center;
  @apply items-center;
  @apply text-white;
  @apply relative;

  height: 810px;
  background: url('/images/img-hero.png');
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;

  .textbox {
    @apply relative;
    @apply text-center;
    @apply mb-20;

    .punchline {
      @apply text-3xl;
      @apply font-semibold;
      @apply leading-relaxed;
      @apply mb-10;
    }

    .link {
      @apply flex;
      @apply justify-center;

      .icon-wrapper {
        @apply h-16 w-16;
        @apply border;
        @apply border-white;
        @apply flex;
        @apply justify-center;
        @apply items-center;
        @apply rounded-full;
      }
    }
  }

  .indicator {
    @apply flex;
    @apply justify-between;
    @apply items-center;
    @apply px-20;

    .item {
      @apply inline-block;
      @apply h-2 w-2;
      @apply border;
      @apply border-white;
      @apply rounded-full;
      @apply transition-colors;
      @apply duration-300;

      &.active {
        @apply bg-white;
      }
    }
  }

  .stick-overlay {
    @apply absolute;
    @apply z-0;

    &.stick-top {
      top: -75%;
      left: -75%;
      transform: scale(0.6) rotate(180deg);
    }

    &.stick-bottom {
      @apply block;
      bottom: -80%;
      left: -5%;
      transform: scale(0.6);
    }

    &::v-deep svg path {
      @apply transition-all;
      @apply duration-1000;
    }

    &.default ::v-deep svg path {
      fill: rgba(0, 40, 255, 0.5);
    }

    &.blue ::v-deep svg path {
      fill: rgba(33, 167, 252, 0.3);
    }

    &.purple ::v-deep svg path {
      fill: rgba(119, 41, 251, 0.3);
    }
  }
}
</style>

Upvotes: 1

Views: 5985

Answers (3)

Yura
Yura

Reputation: 2248

I finally got it fixed by wrapping the <h1> element inside a <template> tag and do the loop on it instead of doing it directly from the <transition> tag. Notice that I also added mode="out-in" in the <transition> tag, otherwise it still jumps.

<transition name="fade" mode="out-in">
  <template v-for="(slide, i) in slides">
    <h1 class="punchline" :key="slide" v-if="current == i">
      {{ slide }}
    </h1>
  </template>
</transition>

The reason of the jumps is because when I inspect the element, the new content is mounted while the old element was still there so it just rendered below the old one before it got completely removed. Not sure why is this happen but probably because <transition> is not meant to be looped.

<transition-group> like this:

<transition-group name="fade" mode="out-in">
  <h1
    class="punchline"
    v-for="(slide, i) in slides"
    :key="slide"
    v-show="current == i">
    {{ slide }}
  </h1>
</transition-group>

also won't work with this condition since it produces the same behavior. (or perhaps i'm using it wrong? please let me know)

Upvotes: 2

kissu
kissu

Reputation: 46676

Alright, I got it !
Sorry, I could not find a working codesandbox with Nuxt + SCSS + Tailwind working, so I pretty much did a page in my own Nuxt project, that means that I had to update some styling since I do not have all the default Tailwind properties in my config file.

Feel free to ditch all the styling if you want since the error is not coming from the styling. Here is the actual answer to your issue with the complete code (minus components I did not have given).

<template>
  <header class="hero">
    <div class="container">
      <div class="textbox">
        <transition name="fade">
          <h1 class="punchline">{{ slides[current] }}</h1> <!-- this is working ! -->
        </transition>
        <NuxtLink class="link" to="/">
          <div class="icon-wrapper">
            tastyicon
            <!-- <Icon icon="chevron-right" /> -->
          </div>
        </NuxtLink>
      </div>
      <div class="indicator">
        <span
          v-for="(slide, i) in slides"
          :key="slide"
          :class="{ active: current == i }"
          class="item"
          @click="animateTo(i)"
        />
      </div>
    </div>
    <!-- <Vector :class="color" class="stick-overlay stick-top" file="model-stick" /> -->
    <!-- <Vector :class="color" class="stick-overlay stick-bottom" file="model-stick" /> -->
  </header>
</template>

<script>
export default {
  data() {
    return {
      current: 0,
      slides: ['Slide 001', 'Slide 002', 'Slide 003'],
    }
  },
  computed: {
    color() {
      if (this.current === 1) return 'blue'
      if (this.current === 2) return 'purple'
      return 'default'
    },
  },
  methods: {
    animateTo(index) {
      this.current = index
    },
  },
}
</script>
<style lang="scss" scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}

.hero {
  @apply flex justify-center items-center text-white relative;

  background-position: center;
  background-repeat: no-repeat;
  background-size: cover;
  height: 810px;
  background: url('https://images.unsplash.com/photo-1612712779378-58eb8b588761?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=1650&q=80');

  .textbox {
    @apply relative text-center mb-20;

    .punchline {
      @apply text-3xl font-semibold leading-relaxed mb-8;
    }

    .link {
      @apply flex justify-center;

      .icon-wrapper {
        @apply h-16 w-16 border border-white flex justify-center items-center rounded-full;
      }
    }
  }

  .indicator {
    @apply flex justify-between items-center px-20;

    .item {
      @apply inline-block h-2 w-2 border border-white rounded-full transition-colors duration-300;

      &.active {
        @apply bg-white;
      }
    }
  }

  .stick-overlay {
    @apply absolute z-0;

    &.stick-top {
      left: -75%;
      top: -75%;
      transform: scale(0.6) rotate(180deg);
    }

    &.stick-bottom {
      @apply block;

      bottom: -80%;
      left: -5%;
      transform: scale(0.6);
    }

    &::v-deep svg path {
      @apply transition-all duration-1000;
    }

    &.default ::v-deep svg path {
      fill: rgba(0, 40, 255, 0.5);
    }

    &.blue ::v-deep svg path {
      fill: rgba(33, 167, 252, 0.3);
    }

    &.purple ::v-deep svg path {
      fill: rgba(119, 41, 251, 0.3);
    }
  }
}
</style>

Here, the issue was how you handled the actual selected element to be displayed on the view depending of it's state. You should really not rely on index in a for loop since it will have unexpected behavior.
The v-if="current == i" got you on this one. Try to display element with something else than an index, those can actually "move" and the DOM will remove them, then bring them back in a poorly manner, hence the ugly transition in your question.
Since you do have current, use it directly like slides[current] !

PS: not sure which kind of configuration you have on your project, but you can totally write your styling like I did (on one line of @apply). Or directly into your template but I guess that you do not like it this way.

Upvotes: 1

anatolhiman
anatolhiman

Reputation: 1859

If this is Vue3 with Vue Router, make sure you use this as your main router view if you want transitions for each page:

  <router-view v-slot="{ Component }">
    <transition>
      <component :is="Component"></component>
    </transition>
  </router-view>

For these I use this CSS [Edit: I see you have these already]:

.v-enter-active {
  transition: opacity 0.8s;
}

.v-enter-from {
  opacity: 0;
}

The jump usually happens while one element leaves (fade-leave-active) and one enters, so it helps to hide each of them in both directions, something like this:

.fade-leave-active,
.fade-enter-active {
  transition: opacity 0.8s;
}

.fade-leave-to,
.fade-enter-from {
  opacity: 0;
}

It helps to keep more heavy logic like v-for away from the transition element, it can cause unexpected content shifting. I try to split things up as much as possible and run loops outside or inside of the transition. It's not as clean as using one transition for everything on the page, but in my experience it is somewhat necessary in order to avoid the type of issue you are experiencing.

<transition name="fade">
   <top-notification class="error" v-if="isError && topNotification">{{ topNotification }}</top-notification>
</transition>

<transition name="fade">
   <top-notification v-if="!isError && topNotification">{{ topNotification }}</top-notification>
</transition>

Lastly, if you use Vuex you have to experiment a bit to get your states as clean as possible and avoid double states being introduced for a split second while logic happens on your page. If v-ifs have more than one condition on them, it happens that both conditions are true at the same time before one of them becomes false, for example. This can lead to the element sticking around for a tiny moment instead of being disappearing directly. So keep your Vue inspector panel open and monitor the state.

Upvotes: 2

Related Questions