All articles
Design & UXMay 13, 2026 6 min read

Motion Ratios That Don't Make Your UI Feel Cheap

Most UI animation feels off because the durations and easing curves are guessed, not designed. Here's the ratio system we use to make motion feel intentional across web and mobile.

Motion Ratios That Don't Make Your UI Feel Cheap

There's a specific kind of cheapness you feel the moment a modal slides in too slowly, or a button bounces like it's auditioning for a children's app. The animation isn't broken — it's just uncalibrated. After enough projects, we stopped guessing durations and started treating motion like a token system with ratios, the same way typography uses a modular scale.

This is the system we reach for when a design lead says "the animations feel off, but I can't tell you why."

Why "feels off" is almost always a ratio problem

When motion feels wrong, engineers tend to fiddle with one number at a time. The modal is too slow, so we drop it from 400ms to 250ms. Now the backdrop fade looks disconnected. We speed that up too. Then the close animation feels abrupt, so we add a spring. Three weeks later the app has 17 different durations and no one remembers why.

The fix isn't picking better numbers. It's picking fewer numbers and applying them in proportional relationships.

A motion system needs three things:

  1. A small set of base durations (usually 4–6)
  2. A small set of easing curves (3 is enough)
  3. Rules about which combinations go with which interaction types

That's it. Everything else — staggers, delays, exits — is derived.

The duration scale we actually use

We borrow the same logic as a type scale: pick a base, then multiply. Our default base is 100ms, with a 1.5× ratio rounded to friendly numbers.

// motion.tokens.ts
export const duration = {
  instant: 80,   // hover states, focus rings, tooltips
  fast:    160,  // small UI: chips, switches, dropdown items
  base:    240,  // standard: buttons, inputs, accordions
  medium:  360,  // panels, drawers, modal content
  slow:    520,  // page transitions, hero reveals
  glacial: 800,  // onboarding flourishes, empty-state intros
} as const;

The rule we enforce in code review: no animation gets a custom duration. If something doesn't fit, we argue about which bucket it belongs to, not which number is right. That single constraint kills 80% of the inconsistency.

Why 100ms is the floor

Anything under ~80ms reads as instant to the human visual system, so it's not really animation — it's just a state change with a cushion. Below that and you're paying a complexity cost for something nobody perceives. We've tested this on slow Android devices too: rendering a 40ms transition and skipping it entirely looks identical to most users.

Why we cap around 500ms for interactive elements

Nielsen Norman has written about this for years, but the practical version: if a user triggered the action, they want to see the result. Past roughly half a second, the animation stops feeling like feedback and starts feeling like waiting. Reserve the longer buckets for content the user didn't ask for — onboarding, page-level transitions, decorative reveals.

Three easings, not thirty

Figma's animation panel lets you pick from a dozen curves. Resist. We use exactly three, named by intent:

:root {
  /* Enter: decelerating — content arrives and settles */
  --ease-enter: cubic-bezier(0.16, 1, 0.3, 1);

  /* Exit: accelerating — content leaves with intent */
  --ease-exit: cubic-bezier(0.7, 0, 0.84, 0);

  /* Move: symmetric — for things that travel between two states */
  --ease-move: cubic-bezier(0.65, 0, 0.35, 1);
}

The semantic naming matters more than the curve values. Once your team thinks in enter / exit / move instead of ease-in-out-back-quart, animation reviews get dramatically faster.

Spring animations have their place — particularly for direct manipulation like drag-and-drop — but for state transitions, well-tuned cubic-béziers are more predictable across devices and easier to reason about in testing.

The pairing rules

This is where ratios earn their keep. Durations and easings aren't picked independently; they're paired by interaction type.

InteractionDurationEasing
Hover / focusinstantmove
Toggle, checkboxfastmove
Button press → resultbaseexit then enter
Modal / drawer openmediumenter
Modal / drawer closebaseexit
Page transitionslowenter

Notice that close is faster than open, by roughly a 2:3 ratio. This is the single highest-leverage rule in the whole system. When users dismiss something, they've already decided to move on — the UI shouldn't make them wait. When something opens, a slightly longer duration gives the eye time to register what arrived.

We got this wrong for years before noticing. Symmetric open/close animations feel sluggish on the way out, every single time.

Stagger ratios for lists

When multiple items animate together — a dropdown menu, a search-results grid, a sidebar of nav items — stagger them at roughly 1/6 of the base duration:

// Framer Motion example
const container = {
  animate: {
    transition: { staggerChildren: 40 / 1000 } // ~base/6
  }
};

const item = {
  initial: { opacity: 0, y: 8 },
  animate: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.24, ease: [0.16, 1, 0.3, 1] }
  }
};

For lists longer than 8 items, cap the total stagger window — otherwise the last item lags long enough to feel broken. We usually cap at 320ms regardless of list length and let later items overlap their entrances.

Accessibility isn't optional here

Motion that ignores prefers-reduced-motion will get flagged in any serious accessibility audit, and rightly so. About 35% of users in some studies report sensitivity to motion, ranging from mild distraction to actual vestibular symptoms.

The right pattern isn't to disable motion entirely — that often breaks the perceived causality of state changes. Instead, reduce distance and scale while keeping a short fade:

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

And in component code, branch the animation itself:

const prefersReduced = useReducedMotion();

const variants = prefersReduced
  ? { initial: { opacity: 0 }, animate: { opacity: 1 } }
  : { initial: { opacity: 0, y: 12 }, animate: { opacity: 1, y: 0 } };

Users still get feedback that something changed. They don't get the sliding, scaling, or parallax that triggers symptoms.

Where this falls apart

A ratio system is a tool, not a religion. Three places it tends to break:

Direct manipulation. When a user is dragging something, motion should be 1:1 with their input — no easing, no duration. The system only kicks in when the user lets go.

Loading and progress. Skeletons, spinners, and progress indicators have their own logic driven by perceived performance, not aesthetic ratio. Keep them out of the token system entirely.

Marketing pages. Hero reveals and scroll-driven storytelling have different goals than product UI. The product team's motion tokens shouldn't constrain the landing page, and vice versa. Two systems, clearly scoped.

Auditing an existing codebase

If you're inheriting a codebase with motion sprawl, don't try to fix it all at once. We do a two-week audit:

  1. Grep for every transition, animate, duration, and ease in the codebase.
  2. Bucket the actual values into a histogram. You'll usually find 20+ unique durations clustering around 5 real intentions.
  3. Propose token names matching those clusters.
  4. Replace top-down by component, starting with the most-used primitives (buttons, inputs, modals).

Don't try to consolidate easings and durations simultaneously. Pick one. Durations first, usually — the values are easier to compare than curves.

What we'd do on Monday

If you're inheriting motion chaos, start with a single PR that adds a motion.tokens.ts file with six durations and three easings. Don't refactor anything yet. Just get the vocabulary into the codebase.

Then, in the next sprint, pick one primitive — the button or the modal — and convert it. Measure how much animation code disappeared. Show the diff in design review. The argument for rolling it out across the rest of the system will make itself.

If you want help building motion tokens into a broader design system, our design and engineering teams do this kind of work regularly. The system above is roughly what we ship by default.

#motion#design systems#accessibility#frontend

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project