Back to blog

Smooth Mount Component

August 16, 2024

In this post, I'm going to share with you a component that I use all the time, both at work and in my personal projects. It's an easy way to level up your UI, specifically by making conditionally rendering UI a breeze. Let's get into it.
I call this component Smooth Mount.

Phase 1

Later in this post, I'll show you what SmoothMount has turned into, but it began as a simple combination of AnimatePresence and motion.div from the popular animation library Motion.
import { AnimatePresence, motion } from "motion/react";

interface ISmoothMountProps {
  show: boolean;
}

const SmoothMount = ({ show }: ISmoothMountProps) => {
  return (
    <AnimatePresence>
      {show && (
        <motion.div
          initial={{ height: 0 }}
          animate={{ height: "auto" }}
          exit={{ height: 0 }}
        >
          {children}
        </motion.div>
      )}
    </AnimatePresence>
  );
};
This simply animates the height of some conditional UI as the elements are added/removed from the React tree. Pair this with a nice transition and you've got yourself a simple yet elegant component.

Phase 2

Shortly after creating this component, I started working on a component library at work. I wanted to include SmoothMount as a component, but felt it was missing something. What if I wanted to animate more than the height property? Maybe I wanted a nice fade in/out animation when the component was animating.
I decided to add a couple more properties to the SmoothMount component: fade and scale:
import { AnimatePresence, motion } from "motion/react";

interface ISmoothMountProps {
  show: boolean;
  height?: boolean;
  fade?: boolean;
  scale?: boolean;
}

const SmoothMount = ({
  show,
  height = true,
  fade,
  scale,
}: ISmoothMountProps) => {
  return (
    <AnimatePresence>
      {show && (
        <motion.div
          initial={{
            height: height ? 0 : "auto",
            opacity: fade ? 0 : 1,
            scale: scale ? 0 : 1,
          }}
          animate={{
            height: "auto",
            opacity: 1,
            scale: 1,
          }}
          exit={{
            height: height ? 0 : "auto",
            opacity: fade ? 0 : 1,
            scale: scale ? 0 : 1,
          }}
        >
          {children}
        </motion.div>
      )}
    </AnimatePresence>
  );
};
This gave me a bit more control over how I wanted the animations to look on a per-implementation basis.

Phase 3

Finally, I wanted to make SmoothMount as flexible as possible, so I updated it to:
  1. Extend the motion.div type, enabling the spread of additional props.
  2. Accept props for AnimatePresence, such as mode, initial, and onExitComplete. Here's what that looks like:
import { AnimatePresence, motion, AnimatePresenceProps } from "motion/react";
import { ComponentProps } from "react";

interface ISmoothMountProps
  extends Omit<ComponentProps<typeof motion.div>, "height"> {
  show: boolean;
  height?: boolean;
  fade?: boolean;
  scale?: boolean;
  mode?: AnimatePresenceProps["mode"];
  initialAnimation?: AnimatePresenceProps["initial"];
  onExitComplete?: AnimatePresenceProps["onExitComplete"];
}

const SmoothMount = ({
  show,
  height = true,
  fade,
  scale,
  initial,
  animate,
  exit,
  children,
  // AnimatePresence props
  mode = "popLayout",
  initialAnimation,
  onExitComplete,
  ...props
}: ISmoothMountProps) => {
  return (
    <AnimatePresence
      mode={mode}
      initial={initialAnimation}
      onExitComplete={onExitComplete}
    >
      {show && (
        <motion.div
          initial={{
            height: height ? 0 : "auto",
            opacity: fade ? 0 : 1,
            scale: scale ? 0 : 1,
            ...(initial as {}),
          }}
          animate={{
            height: "auto",
            opacity: 1,
            scale: 1,
            ...(animate as {}),
          }}
          exit={{
            height: height ? 0 : "auto",
            opacity: fade ? 0 : 1,
            scale: scale ? 0 : 1,
            ...(exit as {}),
          }}
          {...props}
        >
          {children}
        </motion.div>
      )}
    </AnimatePresence>
  );
};

export default SmoothMount;
That's it! I created this StackBlitz project as a playground for SmoothMount with some examples.

⚠️ Don't apply padding or margin to SmoothMount

When Motion calculates the height and width it needs to animate on an element, it doesn't take into account the padding of the element. This causes jumpy animations. If you need padding, apply it to a div within SmoothMount.
Incorrect
<SmoothMount
	...
	className="p-8"
>
	Howdy!
</SmoothMount>
Correct
<SmoothMount
	...
>
	<div className="p-8">
		Howdy!
	</div>
</SmoothMount>

Conclusion

I hope you find SmoothMount helpful! It’s a work in progress, and I’ll likely keep enhancing it as I discover new use cases. Feel free to revisit for updates. Until next time, enjoy a cookie! 🫱🍪