question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Height transition

See original GitHub issue

Clear 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

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:2
  • Comments:12 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
kostyfisikcommented, May 18, 2022

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

0reactions
kostyfisikcommented, Sep 14, 2022

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…

Read more comments on GitHub >

github_iconTop Results From Across the Web

How can I transition height: 0; to height: auto; using CSS?
First, the height transition only works between 0 and 100%, two numeric values. Since "auto" is not a numeric value, fractional increments don't ......
Read more >
Using CSS Transitions on the Height Property
The height of an element is one CSS property that often needs to be transitioned. Sometimes, we want a part of an element...
Read more >
Animate "height" with CSS Transitions - CodePen
For animate the "height" of element with CSS Transitions you need use "max-height". If use the "height: auto", the effect not works. Is...
Read more >
Using CSS Transitions on Auto Dimensions
We can't transition height , but we can transition max-height , since it has an explicit value. At any given moment, the actual...
Read more >
Transition - change width and height of an element - W3Schools
height : 100px; background: red; transition: width 2s, height 4s; ... <p>Hover over the div element below, to see the transition effect:</p>
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found