know.2nth.ai Design motion
design · Motion · Skill Leaf

Motion that decorates
is noise.

Animation should communicate state change — entering, leaving, loading, error. Everything else is visual noise that slows down the interface and annoys users who've turned on reduced motion. The craft is knowing when to animate and, more importantly, when not to.

Live CSS transitions Framer Motion Spring physics prefers-reduced-motion

Four tools, one purpose: communicate change.

CSS transitions are the simplest tool — change a property and the browser interpolates between the old and new values. No JavaScript required. Ideal for hover states, colour changes, and layout shifts that need to feel smooth rather than instant.

CSS animations use @keyframes for multi-step sequences. More control than transitions, still zero JS. Good for loading spinners, skeleton pulses, and anything that loops.

The Web Animations API gives JavaScript control over the same engine that runs CSS animations. You get .play(), .pause(), .reverse(), and promises that resolve when the animation finishes. Useful when you need to coordinate animations with logic.

Framer Motion (React) and similar libraries add spring physics, layout animations, gesture handling, and enter/exit transitions via AnimatePresence. They're the right tool when CSS can't express what you need — and overkill when it can.

From CSS to spring physics.

/* CSS transition — the 80% solution */
.card {
  transform: translateY(0);
  opacity: 1;
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
              opacity 0.3s ease;
}
.card:hover {
  transform: translateY(-4px);
}

/* IntersectionObserver fade-in — this site uses this pattern */
.fade-up {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-up.visible {
  opacity: 1;
  transform: translateY(0);
}
// Framer Motion — enter/exit with spring physics
import { motion, AnimatePresence } from 'framer-motion'

const variants = {
  hidden:  { opacity: 0, y: 20, scale: 0.95 },
  visible: { opacity: 1, y: 0,  scale: 1 },
  exit:    { opacity: 0, y: -10, scale: 0.95 },
}

function Toast({ message, isVisible }) {
  return (
    <AnimatePresence>
      {isVisible && (
        <motion.div
          variants={variants}
          initial="hidden"
          animate="visible"
          exit="exit"
          transition={{ type: "spring", stiffness: 300, damping: 25 }}
        >
          {message}
        </motion.div>
      )}
    </AnimatePresence>
  )
}
// IntersectionObserver — vanilla JS fade-in on scroll
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
      }
    });
  },
  { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
);

document.querySelectorAll('.fade-up').forEach(el => observer.observe(el));
ToolBest forAvoid when
CSS transitionsHover, focus, simple state changesEnter/exit animations, complex sequences
CSS @keyframesLoops, loading states, skeleton pulsesAnimations that need JS coordination
Web Animations APIProgrammatic control, chaining, cancellingSimple hover effects (use CSS)
Framer MotionEnter/exit, layout, gestures, springsNon-React projects, simple CSS-solvable motion

Where motion goes wrong.

Ignoring prefers-reduced-motion

Some users get nauseous from parallax and sliding animations. Others have vestibular disorders. @media (prefers-reduced-motion: reduce) must disable or simplify every animation. This is not optional.

Animating layout properties

Animating width, height, top, or left triggers layout recalculation on every frame. Stick to transform and opacity — they run on the GPU compositor and don't trigger reflow.

will-change on everything

will-change: transform tells the browser to promote an element to its own compositor layer. Applying it to every element wastes GPU memory and can cause blurry text. Use it on elements that will actually animate, and remove it when the animation ends.

Entrance animations that delay content

A staggered fade-in that takes 2 seconds to reveal the page content is not a "delightful experience" — it's making users wait. Keep entrance animations under 400ms total. Users came for the content, not the choreography.

When to animate.

Animate when

  • Something enters or exits the DOM — the user needs spatial context for where it came from or went.
  • A state change happens that might be missed — loading, success, error feedback.
  • The layout shifts and you need to bridge the old position to the new one smoothly.
  • You want to reinforce direct manipulation — drag, swipe, pinch responses.

Where motion links in the tree.

Go deeper.