Home Icon
LinkedIn logo
GitHub logo
Profile Picture
ERICWHITEFIELD
front-end developer

How I solved Auto Height CSS Animation Once and For All

It's a long standing problem for CSS which I solved with Animation Frames

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.

The solution I came up with

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.

Here is an abreviated example showing just the critical code

Alt Text

    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
        })
    }

It would be nice funcationality as a re-usable React Component

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>  

Making animation work with the Component lifecycle

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.