Expand-collapse component with animation using <transition> component
23 Jan 2022

In this article, we will build an animated expand-collapse component using VueJS <transition> component. The <transition> component comes with JavaScript Hooks that we can hook up to achieve a technique use in layout animation called the FLIP technique. We can find the literature here on the FLIP technique by David Khourshid.

Requirement

One of the requirement that I wanted to have for this component is to be able to handle arbitrary heights just like the one with Bootstrap’s Collapse functionality. Some of the solutions out there in the wild uses transform-scale to achieve the effect but the effect kind of felt like it is “squeezing” the content. I wanted the effect to be like a sliding drawer, revealing the items without having a visual distortion, which mimics better in real life as we experience it.

Solution

The solution is a <transition> component wrapper with custom JavaScript hooks to handle the height animation.

TransitionExpand.vue
<template>
  <transition
    name="expand"
    @enter="enter"
    @after-enter="afterEnter"
    @leave="leave"
  >
    <slot></slot>
  </transition>
</template>

<script setup lang="ts">
function enter(element: Element, _: () => void) {
  const { width } = getComputedStyle(element);

  if (element instanceof HTMLElement) {
    element.style.setProperty("width", width);
    element.style.setProperty("position", "absolute");
    element.style.setProperty("visibility", "hidden");
    element.style.setProperty("height", "auto");
    const { height } = getComputedStyle(element);

    element.style.setProperty("width", null);
    element.style.setProperty("position", null);
    element.style.setProperty("visibility", null);
    element.style.setProperty("height", "0");

    // Force repaint to make sure the
    // animation is triggered correctly.
    getComputedStyle(element).height;
    requestAnimationFrame(() => {
      element.style.setProperty("height", height);
    });
  }
}

function afterEnter(element: Element) {
  if (element instanceof HTMLElement) {
    element.style.height = "auto";
  }
}

function leave(element: Element, _: () => void) {
  if (element instanceof HTMLElement) {
    const { height } = getComputedStyle(element);
    element.style.setProperty("height", height);
    // Force repaint to make sure the
    // animation is triggered correctly.
    getComputedStyle(element).height;
    requestAnimationFrame(() => {
      element.style.setProperty("height", "0");
    });
  }
}
</script>

<style scoped>
* {
  will-change: height;
  transform: translateZ(0);
  backface-visibility: hidden;
  perspective: 1000px;
}
</style>

<style>
.expand-enter-active,
.expand-leave-active {
  overflow: hidden;
  transition-property: opacity, height;
  transition-duration: 0.5s;
  transition-timing-function: ease-in-out;
}
.expand-enter-from,
.expand-leave-to {
  height: 0;
  opacity: 0;
}
</style>

Note the following CSS in the above code:

<style scoped>
* {
  will-change: height;
  transform: translateZ(0);
  backface-visibility: hidden;
  perspective: 1000px;
}
</style>

This will actually boost performance by using the GPU.

Using TransitionExpand.vue

To use the component is pretty straightfoward.

<script setup lang="ts">
import { ref } from "vue";
import TransitionExpand from "./TransitionExpand.vue";

const isExpanded = ref(true);
</script>

<template>
  <button @click="isExpanded = !isExpanded">Toggle</button>
  <TransitionExpand>
    <div v-if="isExpanded">...</div>
  </TransitionExpand>
</template>

Accordion Demo

One of the best use-case for the TransitionExpand component is the Accordion control. Try clicking on the Accordion title to see the expand-collapse animation in action. Notice how the animation smoothly hides and reveals without scaling the content.

Lorem ipsum dolor, sit amet consectetur adipisicing elit. Cum corporis repellendus excepturi nulla, quam dolor nihil repellat ipsum dignissimos voluptates blanditiis doloribus, fuga molestiae expedita cupiditate eius itaque similique quia.

Please note that as this component animates the height property, it should be used sparingly as it might cause UI rendering performance issues as its affects the layout if used heavily.