Back to blog

Crafting Smooth Animations for the Web

February 18, 20266 min read

Animations on the web have come a long way. What was once limited to jQuery's .animate() and CSS keyframes has evolved into a rich ecosystem of tools that let us create fluid, natural-feeling motion. But with great power comes great responsibility — and a lot of ways to make things janky.

After years of building interfaces with motion, here's what I've learned about creating animations that feel right.

Why Spring Physics Matter

Most CSS animations use cubic-bezier easing curves. They work fine for simple transitions, but they don't feel the way real objects move. Physical objects have mass, velocity, and friction — they overshoot, settle, and respond to interruption.

Spring-based animations model this behavior. Instead of defining a duration and easing, you define stiffness (how snappy) and damping (how bouncy). The animation calculates itself frame by frame.

// A spring-based animation with Framer Motion
<motion.div
  animate={{ scale: 1 }}
  transition={{
    type: "spring",
    stiffness: 400,
    damping: 25,
  }}
/>

The beautiful thing about springs: they're interruptible. If you change the target mid-animation, it smoothly redirects without any jarring restart. This makes them ideal for hover effects, drag interactions, and layout transitions.

The Layout Animation Trick

One of the most powerful patterns I use regularly is Framer Motion's layoutId. It lets you animate between two completely different DOM elements as if they were the same one.

// Card in a grid
<motion.div layoutId={`card-${id}`}>
  <h3>{title}</h3>
</motion.div>

// Same card, expanded as an overlay
<motion.div layoutId={`card-${id}`}>
  <h3>{title}</h3>
  <p>{fullDescription}</p>
</motion.div>

The browser calculates the positional and dimensional differences between the two states and animates smoothly between them. This is the technique behind the morphing cards on my portfolio.

Performance Checklist

Not all CSS properties animate equally. Here's my hierarchy:

  1. Transform and opacity — Composited by the GPU. Always prefer these. translate, scale, rotate, and opacity are essentially free.
  2. Filter propertiesblur(), brightness(), etc. These are GPU-composited on most browsers but can be expensive with large elements.
  3. Everything elsewidth, height, padding, border-radius — these trigger layout recalculation. Avoid animating them directly.

A practical rule: if you can achieve the same visual effect with transform instead of width/height, always choose transform.

Timing Is Everything

The difference between an animation that feels polished and one that feels sluggish often comes down to tiny timing details:

Accessibility Considerations

Always respect the prefers-reduced-motion media query. Not everyone wants or can comfortably view animations.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

In Framer Motion, you can use the useReducedMotion hook and conditionally apply simpler transitions or skip them entirely.

Wrapping Up

Great animations aren't about showing off — they're about making the interface feel alive and guiding the user's attention naturally. Start with springs, stick to compositable properties, get the timing right, and always keep accessibility in mind.

The best animation is the one the user doesn't consciously notice but would miss if it were gone.