How to Implement Asymmetric Enter/Exit Timing in UI Animations

Asymmetric enter/exit timing uses slower animations (200–400 ms) for deliberate user actions and faster animations (80–150 ms) for system responses to create natural, intuitive interactions.

This technique is a core standard in the emilkowalski/skills repository, which defines animation best practices for modern React applications. By distinguishing between the deliberate phase (when a user presses, holds, or confirms) and the system response (the immediate feedback that follows), you can craft interfaces that feel both responsive and polished.

Core Principles of Asymmetric Timing

The repository’s guidelines in skills/review-animations/STANDARDS.md establish that symmetric timing on press-and-release interactions feels mechanical. Instead, animations should follow the psychological weight of the action.

Deliberate Actions vs. System Responses

According to skills/emil-design-eng/SKILL.md, you must first identify the interaction type:

  • Deliberate actions include opening a modal, expanding a menu, or confirming a destructive operation. These require slower, richer animations to acknowledge the user’s intent.
  • System responses include toast dismissals, quick feedback states, or closing actions. These should snap quickly to maintain interface responsiveness.

Duration and Easing Guidelines

The skills/review-animations/SKILL.md file specifies concrete timing values:

  • Enter (deliberate): Use durations between 200–400 ms with gentle easing such as ease-out or a spring with low stiffness.
  • Exit (response): Use short durations between 80–150 ms with sharp easing like ease-in or a high-stiffness spring.

As noted in skills/review-animations/STANDARDS.md, no hard-coded formula exists for every component. The exact values require iterative tweaking until the "deliberate-slow / snap-fast" rhythm feels natural.

Implementing Asymmetric Timing in Framer Motion

The skills/pick-ui-library/SKILL.md identifies Framer Motion (motion package) as the preferred library for UI-level animations. Define separate motion variants for enter and exit states:

import { motion } from "motion/react";

const variants = {
  // Deliberate entry (slow, easing-out)
  enter: {
    opacity: 1,
    scale: 1,
    transition: {
      duration: 0.35,        // ≈ 350 ms
      ease: "easeOut",
    },
  },

  // System response exit (fast, easing-in)
  exit: {
    opacity: 0,
    scale: 0.95,
    transition: {
      duration: 0.12,        // ≈ 120 ms
      ease: "easeIn",
    },
  },
};

export const Popover = ({ isOpen, onClose }) => (
  <motion.div
    className="popover"
    initial={{ opacity: 0, scale: 0.9 }}
    animate={isOpen ? "enter" : "exit"}
    variants={variants}
    style={{ transformOrigin: "var(--transform-origin)" }}
  >
    {/* content */}
    <button onClick={onClose}>Close</button>
  </motion.div>
);

The enter variant uses a longer duration with easeOut to create a sense of weight, while the exit variant snaps quickly with easeIn to clear the interface efficiently.

CSS-Only Implementation Strategy

If you cannot use Framer Motion, implement asymmetric timing using CSS classes and transitions. The repository standards support this fallback approach:

/* Deliberate entry - slow and gentle */
.popover.entering {
  opacity: 1;
  transform: scale(1);
  transition: opacity 350ms ease-out, transform 350ms ease-out;
}

/* System response exit - fast and sharp */
.popover.exiting {
  opacity: 0;
  transform: scale(0.95);
  transition: opacity 120ms ease-in, transform 120ms ease-in;
}

/* Anchor origin to the trigger element */
.popover {
  transform-origin: var(--transform-origin);
}

Set the --transform-origin custom property via JavaScript based on the trigger element’s position to maintain spatial consistency.

Advanced Patterns: Staggered Lists with Asymmetric Timing

For list components where multiple items enter and exit, apply asymmetric timing at the individual item level while staggering the deliberate entrance:

const listVariants = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.05,
    },
  },
};

const itemVariants = {
  hidden: { opacity: 0, y: 10 },
  visible: {
    opacity: 1,
    y: 0,
    transition: {
      duration: 0.3,
      ease: "easeOut",
    },
  },
  exit: {
    opacity: 0,
    y: -10,
    transition: {
      duration: 0.12,
      ease: "easeIn",
    },
  },
};

<motion.ul variants={listVariants} initial="hidden" animate="visible">
  {items.map(item => (
    <motion.li key={item.id} variants={itemVariants}>
      {item.content}
    </motion.li>
  ))}
</motion.ul>

Each list item inherits the asymmetric pattern: deliberate entry at 300 ms with easeOut and rapid exit at 120 ms with easeIn.

Spatial Consistency with Origin-Aware Animation

Asymmetric timing must pair with origin-aware animation to feel grounded. According to skills/animation-vocabulary/SKILL.md and skills/review-animations/SKILL.md, use a trigger-anchored transform-origin so animations emanate from the UI element that initiated the motion.

Set transform-origin: var(--transform-origin) in your styles and calculate the origin value based on the trigger element’s coordinates. This spatial consistency prevents the asymmetric timing from feeling disconnected or floating.

Summary

  • Asymmetric enter/exit timing distinguishes deliberate user actions (slow, 200–400 ms) from system responses (fast, 80–150 ms).
  • The emilkowalski/skills repository defines these standards in skills/review-animations/STANDARDS.md and skills/emil-design-eng/SKILL.md.
  • Use Framer Motion variants with separate enter and exit transition configurations as the primary implementation method.
  • Maintain spatial consistency by anchoring transform-origin to the trigger element.
  • Iterate on timing values through trial-and-error rather than relying on rigid formulas.

Frequently Asked Questions

The repository suggests roughly a 3:1 to 4:1 ratio, with enter animations typically lasting 200–400 ms and exit animations lasting 80–150 ms. However, skills/review-animations/STANDARDS.md emphasizes that these are starting points; the exact values require testing the component in isolation until the rhythm feels natural.

Why should deliberate actions animate slower than system responses?

Slower enter animations acknowledge the psychological weight of a deliberate choice, giving users time to perceive that their intent has been registered. Fast exit animations respect the user’s time by clearing the interface immediately after the system responds, preventing the interface from feeling sluggish or unresponsive.

Can I use CSS transitions instead of Framer Motion for asymmetric timing?

Yes. While skills/pick-ui-library/SKILL.md recommends Framer Motion for complex UI animations, the asymmetric timing principle works with any animation system. Use CSS classes (like .entering and .exiting) with different transition properties to achieve the same 200–400 ms deliberate entry and 80–150 ms system exit timing.

How do I determine the exact timing values for my specific component?

The repository guidelines explicitly state that no universal formula exists. You should run the component in isolation and adjust durations iteratively. Test with real users or observe the interaction yourself—the goal is a "deliberate-slow / snap-fast" rhythm that feels physically natural for your specific interaction pattern.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →