Skip to content

Instantly share code, notes, and snippets.

@lorado
Last active February 1, 2023 04:26
Show Gist options
  • Save lorado/0dde32c3d769a5f2163f48f8d7496a18 to your computer and use it in GitHub Desktop.
Save lorado/0dde32c3d769a5f2163f48f8d7496a18 to your computer and use it in GitHub Desktop.
Version of TransitionExpand for Vue3/Nuxt3

This gist contains different implementations of TransitionExpand found for Vue2 here. Feel free to read his article also, to get idea how he got to the result.

  • TransitionExpandOriginal.vue contains original code ported to Vue3. Before hide element it applies same width, setting position absolute with hidden visbility. Doing so DOM element is rendered invisibly, so that its possible to get the element height.
  • TransitionExpandAlternative.vue on the other side uses elemnts scrollHeight as the final target height. This approach was ispired by vue3-slude-up-down. I like this one more, as it has much less lines and less operations.
  • TransitionExpandTailwind.vue the last implementation is based on the Alternative solution, while using tailwind instead of custom written styles. Here I also added props to customize speed of opening/closing animation from the parent component.

Usage

<transition-expand>
  <div v-if="isVisible">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
  </div>
</transition-expand>

By changing isVisible boolean, the content will expand/collaps.

Things to take care of

  • Transition allows you to have only one child element at a time. You still can use two elements with v-if and v-else so that either one or another is displayed, but only one may be displayed at once.
  • Avoid padding and margin on the child element. In case you have margin/padding, wrap it with another <div>, so that whole block including padding/margin is animated.

Hardware acceleration

In the original code following CSS is used to try to force hardware acceleration for height animation:

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

While this is working code, I would be careful with this. In case you don't need it - leave it, as the browser starts to use much more resources. So before use - try with and without. I don't use it in our code, as current animation (even withou acceleration) is fine for me.

<template>
<transition name="expand" @enter="enter" @after-enter="afterEnter" @leave="leave">
<slot />
</transition>
</template>
<script setup lang="ts">
const enter = (element: HTMLElement) => {
element.style.height = "0px";
requestAnimationFrame(() => {
element.style.height = element.scrollHeight + "px";
});
};
const afterEnter = (element: any) => {
element.style.height = "";
};
const leave = (element: HTMLElement) => {
element.style.height = element.scrollHeight + "px";
requestAnimationFrame(() => {
element.style.height = "0px";
});
};
</script>
<style lang="scss" scoped>
.expand-enter-active,
.expand-leave-active {
transition: height 1s ease-in-out;
overflow: hidden;
}
</style>
<template>
<transition name="expand" @enter="enter" @after-enter="afterEnter" @leave="leave">
<slot />
</transition>
</template>
<script setup lang="ts">
const enter = (element: HTMLElement) => {
const { width } = getComputedStyle(element);
element.style.width = width;
element.style.position = `absolute`;
element.style.visibility = `hidden`;
element.style.height = `auto`;
const { height } = getComputedStyle(element);
element.style.width = "";
element.style.position = "";
element.style.visibility = "";
element.style.height = "0px";
// Force repaint to make sure the animation is triggered correctly.
getComputedStyle(element).height;
requestAnimationFrame(() => {
element.style.height = height;
});
};
const afterEnter = (element: any) => {
element.style.height = `auto`;
};
const leave = (element: HTMLElement) => {
const { height } = getComputedStyle(element);
element.style.height = height;
// Force repaint to make sure the animation is triggered correctly.
getComputedStyle(element).height;
requestAnimationFrame(() => {
// eslint-disable-next-line no-param-reassign
element.style.height = "0px";
});
};
</script>
<style scoped>
* {
will-change: height;
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}
</style>
<style>
.expand-enter-active,
.expand-leave-active {
transition: height 1s ease-in-out;
overflow: hidden;
}
.expand-enter,
.expand-leave-to {
height: 0;
}
</style>
<template>
<transition
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
:enter-active-class="`transition-all ${enterDurationClass} overflow-hidden`"
:leave-active-class="`transition-all ${leaveDurationClass} overflow-hidden`"
>
<slot />
</transition>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
enterDurationClass?: string;
leaveDurationClass?: string;
}>(),
{
enterDurationClass: "duration-500",
leaveDurationClass: "duration-500",
}
);
const enter = (element: HTMLElement) => {
element.style.height = "0px";
requestAnimationFrame(() => {
element.style.height = element.scrollHeight + "px";
});
};
const afterEnter = (element: any) => {
element.style.height = "";
};
const leave = (element: HTMLElement) => {
element.style.height = element.scrollHeight + "px";
requestAnimationFrame(() => {
element.style.height = "0px";
});
};
</script>
@mrleblanc101
Copy link

mrleblanc101 commented Feb 1, 2023

Hey, thank you so much !
I'm currently trying to put this into a plugin which I'll publish on NPM.
I've made the duration a props which remove the need for a tailwind version entirely.
I'm wondering, why use scrollHeight instead of getBoundingClientRect which would include the padding and remove the need for this warning:

Avoid padding and margin on the child element. In case you have margin/padding, wrap it with another <div>, so that whole block including padding/margin is animated.

@mrleblanc101
Copy link

mrleblanc101 commented Feb 1, 2023

Or maybe we could add a wrapper ?
Something like this, and use the red instead of the element directly ?
But that would require moving the v-if from the div to the component as a v-model I guess

<transition name="expand" @enter="enter" @after-enter="afterEnter" @leave="leave">
  <div ref="element" v-if="modelValue">
    <slot />
  </div>
</transition>

@lorado
Copy link
Author

lorado commented Feb 1, 2023

I'm wondering, why use scrollHeight instead of getBoundingClientRect which would include the padding and remove the need for this warning:

The limitation is not in my code, but in how browser renders elements. If you insert dom element with height:0 but margin, margin will be still there. Depending on box-sizing css parameter (which is default to "content-box") padding will also be visible. Because we enter and remove element from DOM, your content jumps.

@lorado
Copy link
Author

lorado commented Feb 1, 2023

Or maybe we could add a wrapper ?
Something like this, and use the red instead of the element directly ?
But that would require moving the v-if from the div to the component as a v-model I guess

Exactly, and this is something what was made differently here in comparison with previous plugin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment