Animation That Works: Making Interfaces Feel Alive

Damola Oladipo
··7 min read

Most UI animation is decoration. The best animation is communication. Here's how I think about motion in product interfaces and what separates good animation from noise.

TypescriptProduct ThinkingGrowth Systems
Animation That Works: Making Interfaces Feel Alive

There's a version of UI animation that makes designers proud and users annoyed. You've seen it — the landing page where every element fades in as you scroll, each with a slightly different delay, creating a cascade of movement that takes three seconds to resolve before you can read anything. It looks great in a Dribbble shot. It's exhausting to experience.

Good animation is different. Good animation communicates. It answers questions the user didn't know they were asking: Where did that go? What just happened? What's coming next? When animation does that work, users don't notice it. They just feel oriented.

The Purpose Question

Before writing a single animation, ask: what is this communicating?

There are five things animation can usefully communicate in a UI:

  1. State change — something turned on, off, selected, or deselected
  2. Spatial relationship — a panel slid in from the right, so I can go back left
  3. Causality — I tapped this button and that response appeared
  4. Hierarchy — this element is more important than that one
  5. Progress — something is happening and hasn't finished yet

If your animation isn't doing one of these, it's decoration. Decoration has a cost — attention, performance, and sometimes accessibility. It needs to earn its place.

Duration and Easing

This is where most animation goes wrong. The defaults in most libraries are too slow and too bouncy for production interfaces.

My baselines:

InteractionDuration
Micro-interaction (hover, focus)100–150ms
Element entry / exit200–300ms
Page transition300–400ms
Modal / sheet open250–350ms

Anything over 400ms will feel sluggish to most users. Anything under 100ms will feel instant — which is sometimes what you want, but usually not for visibility changes.

For easing, I default to ease-out for entries (quick start, graceful settle) and ease-in for exits (slow start, fast departure). The thing entering deserves attention; the thing leaving doesn't need it.

// Framer Motion — entry/exit pattern I use everywhere
const variants = {
    hidden: { opacity: 0, y: 6 },
    visible: {
        opacity: 1,
        y: 0,
        transition: { duration: 0.25, ease: 'easeOut' },
    },
    exit: {
        opacity: 0,
        y: -4,
        transition: { duration: 0.15, ease: 'easeIn' },
    },
};

<motion.div variants={variants} initial="hidden" animate="visible" exit="exit">
    {children}
</motion.div>;

Framer Motion in Practice

I use Framer Motion for most animation work in React. Its declarative model maps well to how I think about animation states. A few patterns I reach for constantly:

Layout animations

The magic trick. Add layout to a component and Framer Motion automatically animates between layout changes — items reordering in a list, a container expanding, a sidebar opening. This is the hardest animation to write manually and trivially easy with Framer.

{
    items.map((item) => (
        <motion.div key={item.id} layout>
            {item.content}
        </motion.div>
    ));
}

AnimatePresence for exit animations

React removes elements from the DOM immediately. AnimatePresence keeps them alive long enough to play an exit animation, then removes them.

<AnimatePresence>
    {isOpen && (
        <motion.div
            key="modal"
            initial={{ opacity: 0, scale: 0.97 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.97 }}
        >
            <Modal />
        </motion.div>
    )}
</AnimatePresence>

Stagger children

When a list of items appears, staggering their entry adds rhythm without feeling chaotic. Keep the stagger tight — 40–60ms between items is enough to read the cascade.

const container = {
    visible: {
        transition: { staggerChildren: 0.05 },
    },
};

const item = {
    hidden: { opacity: 0, y: 8 },
    visible: { opacity: 1, y: 0 },
};

CSS Transitions vs. JavaScript Animation

Not everything needs Framer Motion. For simple hover states, focus rings, and color transitions, CSS transition is faster, simpler, and composable with Tailwind:

/* Tailwind shorthand */
className="transition-colors duration-150 hover:bg-muted"

I reach for JavaScript animation (Framer Motion) when I need:

  • Exit animations
  • Spring physics
  • Gesture-driven animation (drag, scroll)
  • Complex sequencing or orchestration

Everything else: CSS transitions.

Accessibility

prefers-reduced-motion is not optional.1 Users who have set this preference have told the OS they find motion uncomfortable. Ignoring it is actively harmful for some users.

In Tailwind:

className="transition-transform motion-reduce:transition-none"

In Framer Motion:

import { useReducedMotion } from 'framer-motion';

function AnimatedComponent() {
    const reduce = useReducedMotion();
    return <motion.div animate={{ x: reduce ? 0 : 100 }} />;
}

The best animation is the one you notice only when it's gone. If removing an animation makes an interface feel abrupt, broken, or disorienting — that animation was doing real work. Keep it. If removing it changes nothing, remove it.2

Footnotes

  1. According to the Vestibular Disorders Association, approximately 35% of adults over 40 in the United States have experienced some form of vestibular dysfunction. For these users, parallax scrolling and constant motion can cause genuine physical discomfort including nausea and dizziness.

  2. This test — "what breaks when I remove it?" — is the clearest way I know to distinguish functional animation from decorative animation. Apply it ruthlessly before shipping.

Our Newsletter

Subscribe now so you don't miss any of the latest updates, and you'll also receive a 20% discount code.

Agree Terms and Conditions

Damola Oladipo - Product and Design Engineer exploring ML and NLP research.