Every so often, I get randomly inspired to code something… experimental. Recently, I felt particularly ambitious, and thought it would be neat to create a self-resizing Card component with fancy animations.

In this post, I’m going to walk through the my thought process in creating this fancy component.

Spoiler alert… The result:

The Objective

To create a Card component that…

Automatically resizes itself based on it’s content

Resizes any time the content changes

Renders a skeleton UI, when there’s no content present

Transition nicely between skeleton 👉 content

Renders and animates at a stable 60fps

And, oh yea!… Do all this ☝️ automatically without having to babysit the Card and manage state or whatever the heck (ugh)

The Tools

For this experiment, I went with the following setup:

Codesandbox, for development

React, for JavaScript component things

Fancy, for styles

AnimeJS, for animations

Note: The principles for building this component aren’t specific to any of the libraries/frameworks specified above. You can achieve the same result with vanilla JavaScript and plain ol’ CSS. I just happen to like these libraries 🤓.

The Breakdown

Below is a sketch that details how the auto-resizing stuff is going to work:

The most important parts (or phases) are when…

Content is added or removed, which triggers the update phase

phase Recalcuating the desired height during the ”MATHS” phase

The Structure

This is essentially the bare bones HTML markup for our self-resizing Card:

< div className = " Card " > < div className = " CardContent " > Content goes here... </ div > </ div >

div.Card renders the fancy styles that make up the Card UI. In addition to fancy borders, padding, and shadows, the styles also need ensure that any overflowing content is hidden. This enables the resize effect to transition smoothly between varying content heights (e.g. Transitioning between a 1 sentence statement to a 10 sentence paragraph).

div.CardContent does not have any styles and contains either the content or the skeleton UI (if there is not content). It’s responsible for providing the height, which is then applied to div.Card .

The Update Phase

We need to recalculate and re-render our Card anything the contents have changed. With React, this can be made available via the componentDidUpdate lifecycle hook.

Once the update is triggered, we’ll need to do some ”MATHS“!

The “MATHS” Phase

We’ll need some JavaScript to calculate the desired height from div.CardContent . Once we have that, we’ll make div.Card move using our animation strategy.

The Simple Version

The implementation should look something like this:

updateHeight ( ) { const { clientHeight } = this . innerNode ; const extraPadding = 20 ; const nextHeight = extraPadding + clientHeight ; anime ( { targets : this . node , height : nextHeight } ) ; }

First, we need to calculate the height that we need to transition to. We can grab it from the div.CardContent element, and in our example, add some extra padding.

const { clientHeight } = this . innerNode const extraPadding = 20 const nextHeight = extraPadding + clientHeight

Next, we need to animate it! Since we’re using AnimeJS, the library automatically transitions the specified property. In our case height, it’s from the current value to the next value.

anime ( { targets : this . node , height : nextHeight , } )

By the way, you can achieve the same effect using the CSS transition property. No fancy schmancy library required.

The Better (More Performant) Version

The Simple Version ☝️ works… with some caveats.

Any update/re-render will trigger the animation calculations, even if the height does not change.

Resizing during mid-resize causes a janky transition. This occurs when an update happens too quickly.

To resolve these issues, we’ll need to do the following:

updateHeight ( ) { const { clientHeight } = this . innerNode ; const extraPadding = 20 ; const nextHeight = extraPadding + clientHeight ; if ( this . height === nextHeight ) return ; this . height = nextHeight ; if ( this . animation ) { this . animation . pause ( ) ; } this . animation = anime ( { targets : this . node , height : nextHeight } ) ; }

To ensure that we don’t unnecessarily run the animation, we can cache the calculated height during the initial occurance. Any subsequent calculations can check against the cached value.

If it’s the same, then don’t do anything! Otherwise, proceed with the animation, and update the cached value. Rinse and repeat!

if ( this . height === nextHeight ) return this . height = nextHeight

We can use the same caching trick with the AnimeJS animation instance. We’ll cache the instance during the initial run. Any subsequent runs will pause the previous animation (if applicable), and update the cached value when we create the new animation.

if ( this . animation ) { this . animation . pause ( ) } this . animation = anime ( { targets : this . node , height : nextHeight , } )

And with that, we have an performant self resizing animating UI.

The Skeletons

Now that the hard part is over (phew), we need to take care of the skeleton UI (aka. blank/empty state). The logic for this is pretty simple! If the Card component has content, then render the content! Otherwise, render our skeleton UI.

An example of this logic may look something like this:

render ( ) { const content = children || < SkeletonUI /> return ( < div className = " Card " > < div className = " CardContent " > { content } </ div > </ div > ) }

The Result

Pretty spiffy, if I do say so myself!

Thankfully, this was a success little experiment. Sometimes I’m not so lucky! However, I always enjoy the process, and most importantly, I always learn something.

Hopefully you found this post enjoyful or helpful in some way! Either way, thanks for checking it out 😊.

Here’s a linky to the source code (for you curious ones out there).

Bonus! Here’s another version with images!