Vuejs transition css only slide down

10.7k views Asked by At

I've been searching for a long time for a solution to this problem, but I can't seem to find the best solution for this problem. Basically I have an accordion which when a user open's it, it should slide down gently with a nice transition.

I'm pretty sure this is possible with only css. Please correct me if I'm wrong, but css transitions are performance wise better than js transitions.

So now the the HTML for the problem I'm facing:

<transition name="slide-down">
  <ul v-if="isDisplayingOpeningHours" class="mt-2 text-sm flex flex-col space-y-2">
    <li v-for="i in 10" :key="i"> <!-- just for testing purposes -->
      element
    </li>
  </ul>
</transition>

And the css is:

.slide-down-enter-active, .slide-down-leave-active {
  transition: all .5s;
}

.slide-down-enter-to, .slide-down-leave {
  overflow: hidden;
  max-height: 100%;
}

.slide-down-enter, .slide-down-leave-to {
  overflow: hidden;
  max-height: 0;
}

This results in a not fluent transition. Sliding down doesn't seem to be a problem, but when it slides up it's really janky and not fluent at all!. If I set the max-height: 10rem it isn't making it any better. I'm hoping someone can debug help me with this problem and maybe educate me more about vue transitions and js vs css transitions.

Edit:
Just for clarification the transition itself should by css only. The opening of the accordion is with js which is fine. Also it would be nice to maybe see an example of a transition use with the native vue-transition component.

Edit 2:
Here is a codesandbox which has the same problem I have. https://codesandbox.io/s/frosty-bash-lmc2f?file=/src/components/HelloWorld.vue

1

There are 1 answers

19
Dipen Shah On BEST ANSWER

Another Update!!! In order to achieve JS transition you can take a look at official document to learn. Anyways, I am not going to implement fancy timing mechanism, but in simplest form, JS collapse transition in react will look something like following, I included it in existing codesandbox:

<template>
...
  <div>
    <button @click="jsOpen = !jsOpen">Open JS list</button>
    <transition
      v-on:before-enter="beforeEnter"
      v-on:enter="enter"
      v-on:after-enter="afterEnter"
      v-on:enter-cancelled="enterCancelled"
      v-on:before-leave="beforeLeave"
      v-on:leave="leave"
      v-on:after-leave="afterLeave"
      v-on:level-cancelled="leaveCancelled"
      v-bind:css="false"
    >
      <ul v-if="jsOpen" class="mt-2 text-sm flex flex-col space-y-2">
        <li v-for="i in 10" :key="i">item-{{ i }}</li>
      </ul>
    </transition>
  </div>
</template>

<script>
export default {
  ...
  methods: {
    beforeEnter(el) {
      el.style.height = 0;
      el.style.overflow = "hidden";
    },
    enter(el, done) {
      const increaseHeight = () => {
        if (el.clientHeight < el.scrollHeight) {
          const height = `${parseInt(el.style.height) + 5}px`;
          el.style.height = height;
        } else {
          clearInterval(this.enterInterval);
          done();
        }
      };
      this.enterInterval = setInterval(increaseHeight, 10);
    },
    afterEnter(el) {},
    enterCancelled(el) {
      clearInterval(this.enterInterval);
    },
    beforeLeave(el) {},
    leave(el, done) {
      const decreaseHeight = () => {
        if (el.clientHeight > 0) {
          const height = `${parseInt(el.style.height) - 5}px`;
          el.style.height = height;
        } else {
          clearInterval(this.leaveInterval);
          done();
        }
      };
      this.leaveInterval = setInterval(decreaseHeight, 10);
    },
    afterLeave(el) {},
    leaveCancelled(el) {
      clearInterval(this.leaveInterval);
    },
  },
};
</script>

Update: After understanding requirements from OP, I was trying to play with different things and I stumbled upon this article on css-tricks which explains in much more details about transition, from that article another non-js solution proposed would look like following, I included it in existing stackblitz:

.scale-enter-active,
.scale-leave-active {
  transform-origin: top;
  transition: transform 0.3s ease-in-out;
}

.scale-enter-to,
.scale-leave-from {
  transform: scaleY(1);
}

.scale-enter-from,
.scale-leave-to {
  transform: scaleY(0);
}

here instead of height, it is scaling against y-axis. IT also has a side effect that it expands container to full size first and than updates (scales) content afterwards.

Vue v3.x -leave and -enter classes in V3 are renamed to -leave-from and -enter-from. Take a look at this stackblitz.

.slidedown-enter-active,
.slidedown-leave-active {
  transition: max-height 0.5s ease-in-out;
}

.slidedown-enter-to,
.slidedown-leave-from {
  overflow: hidden;
  max-height: 1000px;
}

.slidedown-enter-from,
.slidedown-leave-to {
  overflow: hidden;
  max-height: 0;
}

Vue v2.x Problem is you are specifying max-height to 100%, use some value which will be greater than your expected max-height. Update your style to:

.slide-down-enter-active,
.slide-down-leave-active {
  transition: max-height 0.5s ease-in-out;
}

.slide-down-enter-to,
.slide-down-leave {
  overflow: hidden;
  max-height: 1000px;
}

.slide-down-enter,
.slide-down-leave-to {
  overflow: hidden;
  max-height: 0;
}

and it should work. Take a look at this codesandbox demo.