There's a fundemental problem with animating the height of an HTML element when it's height is set to 'auto'. In fact, solving this problem has even been called the The Holy Grail of CSS Animation.
The problem is that CSS animations require concrete CSS values for the start and end. This is how the animation frames are calculated before the animation even begins. With height:auto it is unclear where the end point will be.
Usually solutions revolve around animating max-height or using scale. Both solutions are bit hacky and can give undesirable results. Max-height animations are still an aproximation and can lead to uneven animation duration or jerky animations. Scale animations work by esentially scalling a image of the rendered area. So they tend to pixilate.
I got to thinking, what if it was possible to ask the browser what the rendered height would be with particular content within it. Then with that information start an animation to that exact height. When it's done auto height can be restored without any visual change occuring.
Fortunately it's possible to do exactly that using Animation Frames.
componentDidMount = () => {
// get the current height
let elWrap = document.getElementById(this.idOnlyChild)
let wrect = elWrap.getBoundingClientRect();
let prevHeight = wrect.height + 'px'
let prevWidth = wrect.width + 'px'
// set all content opacity to zero
// set only the content that will be vislbe after animation to display:block
// other content that will not be visible to display: none;
// Set height:auto and get the new height
elWrap.style.height = 'auto'
let rect = elWrap.getBoundingClientRect();
let newHeight = rect.height
let newWidth = rect.width
// That temporary height is never rendered in the browser
// Restore the original height before any rendering
elWrap.style.height = prevHeight
elWrap.style.width = prevWidth
// Start Animation in next Animation frame from original height to new height
// Reveal the contents via opacity animation
requestAnimationFrame(() => {
elWrap.style.height = newHeight + 'px'
elWrap.style.width = newWidth + 'px'
elTo.style.transitionDelay = this.transitionDelay
elTo.style.opacity = 1
})
}
This functionality is an excelent candidate to be encapsulated using render props. The user of the component does not really need to know how everything works. Plus the event handlers attached to content bind to the user's component.
Here is a conceptual example of three step form. The only requirements for use are a property called 'step', and some content. Navigation from one step to another can occur in any order by setting this.state.step to any value 0 through 2.
<AutoHeightAnimation step={this.state.step} >
<div id="divToAnimate" class="autoHeight whatverStyleYouWant styledComponents etc">
<div onClick={this.goStepTwo}>
<div>Click here to subscribe</div>
</div>
<div>
<div>Please enter your email</div>
<input type="text"></input>
<button onClick={this.goStepThree}>send</button>
</div>
<div>
<div onClick={this.goStepOne}>Thanks</div>
</div>
</div>
</AutoHeightAnimation>
You may have noted that the animation code above runs in componentDidMount. This is done to make the animation compatible with setState() and the re-rendering of DOM elements by React. To animate from one piece of content to another both elements need to be present in the DOM. If React decides to re-create one of these elements we need to let that finnish first otherwise we will be referencing elements that no longer exist.