Motion of the expandable box

Certain interface elements feel natural and intuitive, while others feel jarring or disconnected from user expectations. Motion serves as a fundamental element in modern interfaces, enhancing user experience when implemented thoughtfully. Multiple factors contribute to effective motion design, including timing, easing functions, and sequencing considerations. The expandable box component provides a practical example to examine these essential elements, demonstrating how appropriate motion can improve functional interactions.

The content includes hover-based interactions optimized for cursor control. To view the touch-based version adapted for mobile devices, please open this article on your smartphone or tablet.

This content has been adapted for touch-based mobile devices. For the complete experience including hover interactions, please view on desktop.

Read more
Hey, click me!I'm the component we are going to break down into details...

Readers bring diverse backgrounds and perspectives. While some may seek deeper technical insights or foundational concepts explained, others may want to keep the story simple and concise.

The expandable component solves this challenge by allowing additional details to be seamlessly woven into the main narrative. Readers can quickly gauge the hidden content's relevance to their needs through the thumbnail's title and description, choosing whether to explore further without disrupting their reading flow. This approach ensures the content remains accessible to all while preserving the story's natural progression.

When you hover over the collapsed box, it previews its upcoming transformation. The subtle shift outward – expanding slightly to the left, right, and bottom – serves as a natural prelude to the full expansion that follows your click.

The visual cues are intentionally designed: the left icon rotates outward to signal horizontal expansion, while the right arrow suggests there's more content waiting to unfold vertically. These small but purposeful movements guide users toward the interaction.

The hover effect is swift and decisive, completing in just 220ms with an ease-out timing – meaning it starts quick and gently decelerates. However, the full expansion after clicking takes a more measured approach. Like a heavy object in the physical world, the box needs time to build momentum. This natural motion is achieved through a longer 550ms duration with ease-in-out timing: it starts gradually, builds speed in the middle, and smoothly decelerates at the end. This thoughtful timing creates a sense of mass and physical presence, making the interaction feel more authentic and satisfying.

When clicked, the box expands horizontally and downward while the thumbnail minimizes. The left icon rotates outward, emphasizing the horizontal expansion, while the right icon rotates to indicate the open state.

The expandable box is designed to behave like a substantial object. Similar to heavy objects in the physical world, it needs time to build momentum. The animation completes in approximately 550ms with ease-in-out timing: starting slowly, accelerating in the middle, and gradually decelerating at the end. This deliberate timing gives the impression of mass and physical presence, making the interaction feel more natural and satisfying.

The expansion unfolds in two sequential steps. First, the box smoothly expands to its full height, making a space for the content. Then, the content itself gracefully fades into view. This deliberate sequence – expand first, reveal second – makes the interaction feel more polished and easier to follow.

Expanding animation
Real Time
Read more
Play with me!Hover and click to see the animation in action.

This interactive element is linked with the timeline chart above, showcasing the CSS property changes occurring during each transition phase. You can initiate the animation sequence either by interacting directly with this box or by pressing the play button on the chart. For a customized viewing experience, adjust the animation speed using the tempo selector positioned above the timeline.

Transitions utilize the ease-out and ease-in-out easing functions, which creates animations that begin rapidly and gradually decelerate toward completion. This characteristic may create the visual illusion that the chart playhead continues its movement slightly longer than the actual component animation.

Technical insights

Read more
Orchestrating sequential animationCSS transition delay offer a simple and effective way to sequence animations in CSS without complex JavaScript

Developers can create cascading effects by applying different delay values to elements using the transition-delay property.

index.tsx
transition:
    width 0.22s ease-out,
    height 0.22s ease-out 0.4s,
    transform 0.35s ease-out 0.1s,
    opacity 0.25s ease-out;

Interesting effects can be achieved when the delay is combined with :nth-child(*) selector.

index.tsx
ul li:nth-child(n) {
  transition-delay: calc(0.1s * (n - 1));
}

Read more
PerformanceAnimations performs better when using transformations and opacity changes.

For optimal performance, CSS animations should prioritize transformations (transform) and opacity changes over modifying position properties. These properties trigger GPU acceleration and avoid costly browser reflows, resulting in smoother animations that consume less processing power.


Read more
Animating height with CSSCSS can't animate to "auto" height, but JavaScript measurements save the day.

While it would be convenient to animate an element's height by transitioning between a fixed value and auto, CSS transitions don't support this functionality. If the expanded height is predictable, we can simply transition to that specific value. For elements with an unknown but bounded height, using max-height with a reasonable upper limit works, though this introduces a delay during collapse as the animation must progress through the unused portion of the max-height.

For the most polished solution, measuring the actual content height with JavaScript and dynamically setting the max-height property when expanding provides the best user experience. This approach ensures smooth transitions in both directions without artificial delays, as the animation only needs to progress through the actual content height.

index.tsx
const useElementSize = <T extends HTMLElement>(
    callback: (
        target: T,
        entry: ResizeObserverEntry,
        size: {
            width: number
            height: number
            clientWidth: number
            clientHeight: number
        },
    ) => void,
) => {
    const ref = useRef<T>(null)
    const resizeTimeout = useRef<number | null>(null)

    useLayoutEffect(() => {
        const element = ref?.current

        if (!element) {
            return
        }

        const observer = new ResizeObserver(entries => {
            if (resizeTimeout.current) {
                clearTimeout(resizeTimeout.current)
            }
            resizeTimeout.current = window.setTimeout(() => {
                const entry = entries[0]
                callback(element, entry, {
                    width: element.offsetWidth,
                    height: element.offsetHeight,
                    clientWidth: element.clientWidth,
                    clientHeight: element.clientHeight,
                })
            }, 100)
        })
        observer.observe(element)
        return () => {
            observer.disconnect()
            if (resizeTimeout.current) {
                clearTimeout(resizeTimeout.current)
            }
        }
    }, [callback, ref])

    return ref
}

const MyComponent = ({ children, thumbnail }) => {
    const [isOpen, setIsOpen] = useState(false)
    const [contentHeight, setContentHeight] = useState(76)

    const contentRef = useElementSize<HTMLDivElement>((_element, _entry, { height }) => {
        setContentHeight(height + 76)
    })

    return (
        <div className='my-component'>
            <div data-open={isOpen} style={{ maxHeight: isOpen ? contentHeight : undefined }}>
                <div ref={contentRef} className='my-component-content'>
                    {children}
                </div>
            </div>
        </div>
    )
}

Last edited on Mar 14, 2025 by

Tom Antas