Height transition
See original GitHub issueClear and concise description of the problem
Performant height transition is an old and popular topic, see SO link with >2500 votes. It can be useful for animation of Menu items, showing\hiding form fields, in some vertical accordion, etc. JS nature of Vue makes it easier to solve this problem. However, I am not aware of any free and open-source solution for this in Vue.
Suggested solution
I have created a Vue component to solve this issue. It is in a very early stage of development, but rather usable. See the demo and related GitHub It is based on Web Animation API to ensure GPU usage (no polyfill in demo, so use only modern browser to take a look), so there is no need to use any 3rd party animation lib, and thus it can be included to the core module. Note, that there is also Popmotion based Motion module, which can be another place to host this. Still, the main question is whether this kind of stuff is needed in VueUse at all?
<script setup lang="ts">
interface Props {
duration?: number;
easingEnter?: string;
easingLeave?: string;
opacityClosed?: number;
opacityOpened?: number;
}
const props = withDefaults(defineProps<Props>(), {
duration: 250,
easingEnter: "ease-in-out",
easingLeave: "ease-in-out",
opacityClosed: 0,
opacityOpened: 1,
});
const closed = "0px";
interface initialStyle {
height: string;
width: string;
position: string;
visibility: string;
overflow: string;
paddingTop: string;
paddingBottom: string;
borderTopWidth: string;
borderBottomWidth: string;
marginTop: string;
marginBottom: string;
}
function getElementStyle(element: HTMLElement) {
return {
height: element.style.height,
width: element.style.width,
position: element.style.position,
visibility: element.style.visibility,
overflow: element.style.overflow,
paddingTop: element.style.paddingTop,
paddingBottom: element.style.paddingBottom,
borderTopWidth: element.style.borderTopWidth,
borderBottomWidth: element.style.borderBottomWidth,
marginTop: element.style.marginTop,
marginBottom: element.style.marginBottom,
};
}
function prepareElement(element: HTMLElement, initialStyle: initialStyle) {
const { width } = getComputedStyle(element);
element.style.width = width;
element.style.position = "absolute";
element.style.visibility = "hidden";
element.style.height = "";
let { height } = getComputedStyle(element);
element.style.width = initialStyle.width;
element.style.position = initialStyle.position;
element.style.visibility = initialStyle.visibility;
element.style.height = closed;
element.style.overflow = "hidden";
return initialStyle.height && initialStyle.height != closed
? initialStyle.height
: height;
}
function animateTransition(
element: HTMLElement,
initialStyle: initialStyle,
done: () => void,
keyframes: Keyframe[] | PropertyIndexedKeyframes | null,
options?: number | KeyframeAnimationOptions
) {
const animation = element.animate(keyframes, options);
// Set height to 'auto' to restore it after animation
element.style.height = initialStyle.height;
animation.onfinish = () => {
element.style.overflow = initialStyle.overflow;
done();
};
}
function getEnterKeyframes(height: string, initialStyle: initialStyle) {
return [
{
height: closed,
opacity: props.opacityClosed,
paddingTop: closed,
paddingBottom: closed,
borderTopWidth: closed,
borderBottomWidth: closed,
marginTop: closed,
marginBottom: closed,
},
{
height,
opacity: props.opacityOpened,
paddingTop: initialStyle.paddingTop,
paddingBottom: initialStyle.paddingBottom,
borderTopWidth: initialStyle.borderTopWidth,
borderBottomWidth: initialStyle.borderBottomWidth,
marginTop: initialStyle.marginTop,
marginBottom: initialStyle.marginBottom,
},
];
}
function enterTransition(element: Element, done: () => void) {
const HTMLElement = element as HTMLElement;
const initialStyle = getElementStyle(HTMLElement);
const height = prepareElement(HTMLElement, initialStyle);
const keyframes = getEnterKeyframes(height, initialStyle);
const options = { duration: props.duration, easing: props.easingEnter };
animateTransition(HTMLElement, initialStyle, done, keyframes, options);
}
function leaveTransition(element: Element, done: () => void) {
const HTMLElement = element as HTMLElement;
const initialStyle = getElementStyle(HTMLElement);
const { height } = getComputedStyle(HTMLElement);
HTMLElement.style.height = height;
HTMLElement.style.overflow = "hidden";
const keyframes = getEnterKeyframes(height, initialStyle).reverse();
const options = { duration: props.duration, easing: props.easingLeave };
animateTransition(HTMLElement, initialStyle, done, keyframes, options);
}
</script>
<template>
<Transition :css="false" @enter="enterTransition" @leave="leaveTransition">
<slot />
</Transition>
</template>
Alternative
Simple CSS max-height solution is OK, however, has unpredictable (based on contend rendering result) visual animation duration. ScaleY solution can be a bit messy for some users. Clip solution is good, but it can’t be used when the element is incorporated into the main layout (e.g. for some additional form field to apper on checkbox click), etc.
I started with this blog post and somehow improved it (mostly to keep the initial style of the content block after the end of the transition). The main change is the switch to Web Animation API, which seems as performant as pure CSS animation and provides much more control. This also had eliminated all performance optimization hack from the original solution.
Additional context
If it seems to be a useful addition to VueUse I will be happy to rewrite this code to comply to the project coding style.
Validations
- Follow our Code of Conduct
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn’t already an issue that request the same feature to avoid creating a duplicate.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:2
- Comments:12 (5 by maintainers)
Top GitHub Comments
@antfu @jd-solanki I have prepared an early implementation example, it works, but it still needs to be refactored to become cleaner and to cover all known corner cases. However, you are welcome to provide some early feedback on this, especially on Demo section.
Sorry, I’ve changed the company where I work and got immediately overloaded… I will reopen the issue as soon as I have any updates on this…