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.
<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.
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.