Gijo Varghese
Gijo Varghese

Reputation: 11780

Animate Height on v-if in Vuejs using Transition

I'm using the following code to animate v-if element by reducing the height to 0px from. The animation works fine. But the problem is I've to specify the initial height of the element is CSS. For one element this is ok, But I want to apply this animation to multiple elements. How can I fix this? So that whatever be the height, animation works fine!

<transition name="fadeHeight" mode="out-in">
<div v-if="something">
<p>something over here where the height is not constant</p>
</div>
</transition>

.fadeHeight-enter-active,
.fadeHeight-leave-active {
  transition: all 0.2s;
  height: 230px;
}
.fadeHeight-enter,
.fadeHeight-leave-to
{
  opacity: 0;
  height: 0px;
}

Upvotes: 25

Views: 49656

Answers (4)

T.Woody
T.Woody

Reputation: 1228

@ryantdecker has the most common answer available. I prefer doing less code though and do class binding instead:

<template>
 <!-- isShowing either data or computed... -->
 <div class="foo" :class="{ showing: isShowing, hidden: !isShowing }">
  <p>
   something here where the height is not constant
  </p>
 </div>
</template>
...
<style>
.foo {
 height: auto;
 transition: max-height 0.5s;
 &.showing {
  max-height: 200px; /* MUST BE GREATER THAN height:auto */
 }
 &.hidden {
  max-height: 0px;
 }
}
</style>

A few customizations that can be done for even more control are:

  1. Set :style="{'max-height': computedHeight}"
  2. Use ease-in and ease-out with two different transitions within the .showing and .hidden classes respectively.
  3. Use a cubic bezier transition speed for really long contents that collapse/expand

The first customization above can be used when you are using distinct items, like pictures, flex rows where the height can be seen via devtools and the height calculated. E.g.:

computed: {
 /**
  * @return {string} max height of the container in pixels if shown else zero
  */
 calcedHeight()
 {
   const elHeight = 80;
   const maxHeight = this.isShowing ? elHeight * this.elementCount : 0
   const maxHeightPx = maxHeight + 'px'
   return {
    'max-height': maxHeightPx
   }
 }
}

This could easily be made into a component with isShowing, elHeight, and elCount props at this point.

Cubic Bezier

I am giving this it's own section because it might be all that is needed in regards to crazy long elements (think 5000px max-heights):

&.showing {                                                                                          
   transition: all 0.6s cubic-bezier(1, 0.01, 1, 0.01);                                                 
}                                                                                                       
&.hidden {                                                                                           
   transition: all 0.6s cubic-bezier(0.01, 1, 0.01, 1);                                                 
}

Upvotes: 4

mackishdahacker
mackishdahacker

Reputation: 124

I had some troubles with this one, and many of the answers out there were way to complex IMO. After a while, this is what i came up with to make a smooth height transition for "height: auto" content:

<template>
 <transition name="expand">
   <div v-show="isExpanded" ref="content">
     <slot />
   </div>
 </transition>
</template>

<script setup lang="ts">
import { onMounted, ref } from '@vue'

defineProps<{isExpanded: boolean}>()
const content = ref()
let height = ref()

onMounted(() => {
  height.value = `${content.value.getBoundingClientRect().height}px`
})
</script>

<style scoped lang="less">
.expand-leave-active,
.expand-enter-active {
  transition: all 350ms ease;
  overflow: hidden;
}

.expand-enter-to,
.expand-leave-from {
  height: v-bind(height);
}

.expand-enter-from,
.expand-leave-to {
  opacity: 0;
  height: 0;
}
</style>

Hope this helps someone!

Upvotes: 9

Yair Levy
Yair Levy

Reputation: 1594

As mentioned - maxheight transition is usually the way to solve this issue. but in some cases you might not be able to use maxheight transitions. For those cases you can use a wrapper container component that will make the transition when needed.

<template>
  <div
    class="fluid-wrapper"
    :class="{ 'in-transition': transitionState }"
    :style="computedDimensions"
    @transitionend="transitionState = 0"
  >
    <slot />
  </div>
</template>
<script>
export default {
  name: 'FluidContainer',
  props: ['trigger'],
  data() {
    return {
      oldRect: {
        height: null,
        width: null,
      },
      newRect: {
        height: null,
        width: null,
      },
      transitionState: 0,
      // 0: no Dimensions, no transition
      // 1: oldRect Dimensions, transition is on
      // 2: newRect Dimensions, transition is on
    };
  },
  computed: {
    computedDimensions() {
      if (!this.transitionState) {
        return null;
      }
      return this.transitionState === 1 ? this.oldRect : this.newRect;
    },
    dimensionsHasChanged() {
      return (
        this.newRect.height !== this.oldRect.height
        || this.newRect.width !== this.oldRect.width
      );
    },
  },
  watch: {
    trigger() {
      const oldStyle = getComputedStyle(this.$el);
      this.oldRect.height = oldStyle.height;
      this.oldRect.width = oldStyle.width;
      this.$nextTick(() => {
        const newStyle = getComputedStyle(this.$el);
        this.newRect.height = newStyle.height;
        this.newRect.width = newStyle.width;
        if (this.dimensionsHasChanged) {
          this.transitionState = 1;
          window.requestAnimationFrame(() => {
            this.transitionState = 2;
          });
        } else {
          this.transitionState = 0;
        }
      });
    },
  },
};
</script>

<style lang="scss" scoped>
.fluid-wrapper {
  /* overflow: hidden; */
  height: fit-content;
  width: fit-content;
  &.in-transition {
    transition: all 0.3s;
  }
}
</style>

Usage:

<FluidContainer :trigger="some-variable">
    <!-- Any Reactive Content -->
</FluidContainer>

‘trigger’ prop - is required for this to work. it can be any piece of state that makes the inner content change. the wrapper will watch the trigger in order to detect when a change in dimensions occurs and make the transition.

Upvotes: 0

ryantdecker
ryantdecker

Reputation: 1810

It doesn't look like you've posted all the code, but hopefully I understand the goal.

Try moving the transition to the max-height property:

.fadeHeight-enter-active,
.fadeHeight-leave-active {
  transition: all 0.2s;
  max-height: 230px;
}
.fadeHeight-enter,
.fadeHeight-leave-to
{
  opacity: 0;
  max-height: 0px;
}

as long as you set a max height to be larger than the tallest element, it should accomplish what you need. Note that you may also want to use overflow:hidden as well. If you have dramatic variation of the actual height of the elements, this solution may not be the best, as it will make the animation duration/delay appear very different.

https://jsfiddle.net/7ap15qq0/4/

Upvotes: 46

Related Questions