Implementing Asymmetric Enter/Exit Timing for Deliberate vs Responsive Interactions

Asymmetric enter/exit timing makes UI elements appear slowly when users need to make a decision and disappear instantly when the system must react, typically implemented with CSS transitions using longer durations for :active states and short ease-out curves for default states.

This animation pattern prioritizes user perception over mechanical symmetry. In the emilkowalski/skills repository, the technique is codified as a core principle of design engineering—one that shapes how users experience commitment, confirmation, and control. The following guide breaks down the architectural reasoning, CSS implementation strategies, and production-ready code directly from the source.


The Design Philosophy Behind Asymmetric Timing

The repository's Emil Design Engineering skill file establishes a clear policy: entrance animations should feel deliberate, while exits must feel responsive. This asymmetry serves distinct psychological purposes.

Deliberate entry (<200ms–2s) gives users time to recognize a new UI element and abort if needed. Think of a destructive action that reveals itself only after a hold—users need visual confirmation that something is happening.

Responsive exit (~200ms ease-out) confirms that the system acknowledged the user's action immediately. A slow exit creates perceived lag, even when the underlying operation completed quickly.

This policy is explicitly documented in skills/emil-design-eng/SKILL.md#L87-L90:

"Enter animations give users a moment to decide or notice a new UI element. Release animations must feel instantaneous to reinforce that the system is reacting to the user's action."

Perceptual research supports this: users tolerate anticipation but punish unresponsiveness. The asymmetry trades ** upfront patience for downstream satisfaction**.


Why CSS Transitions Beat Keyframes

The skill file advocates for CSS transitions over @keyframes for one critical reason: retargetability. Unlike keyframe animations that lock to a predefined timeline, transitions can be interrupted and reversed mid-flight without visual glitches.

Consider a hold-to-delete button. If the user releases before the threshold:

  • The "enter" transition toward the delete state stops
  • A new transition toward the idle state begins seamlessly
  • The exit remains fast regardless of how far the enter progressed

This behavior would require complex animation-frame management with keyframes. With transitions, the browser handles interpolation automatically, keeping the animation on the GPU composite layer.

The source emphasizes transform and opacity as the animatable properties—both avoid layout recalculation and paint storms. From SKILL.md#L77-L82:

"Transitions on transform and opacity stay on the GPU, while the asymmetric durations do not introduce layout-thick paints."


Core Implementation Pattern

The canonical structure uses state-driven transition overrides: a base rule for the responsive exit, and a scoped rule for the deliberate enter.

Basic Asymmetric Button

/* Base state: fast, responsive exit */
.button {
  transition: transform 200ms ease-out, opacity 200ms ease-out;
  transform-origin: var(--transform-origin, center);
}

/* Active state: deliberate, slower entry */
.button:active {
  /* Overlay reveals slowly while held */
  transition: clip-path 2s linear;
}

The :active pseudo-class triggers on press/hold, applying the longer duration. Release immediately reverts to the base transition, snapping back or completing the action with the short curve.

Overlay-Style Destructive Action

.button .overlay {
  /* Default: instant dismissal */
  transition: clip-path 200ms ease-out;
  clip-path: circle(0% at center);
}

.button:active .overlay {
  /* Held: gradual reveal */
  transition: clip-path 2s linear;
  clip-path: circle(150% at center);
}

Here the same property animates with different timing curves depending on state direction. The overlay grows slowly to confirm intent, then collapses instantly on release.


Modern Pure-CSS Entry with @starting-style

For entrance animations without JavaScript state management, the repository showcases the @starting-style at-rule. This CSS feature (gaining browser support in 2024) defines starting values for transitions that couldn't otherwise animate from "no style."

.toast {
  /* Final visible state */
  opacity: 1;
  transform: translateY(0);
  transition: 
    opacity 400ms ease, 
    transform 400ms ease;

  /* Starting values for entry transition */
  @starting-style {
    opacity: 0;
    transform: translateY(100%);
  }
}

How it works:

  • Element mounts with opacity: 0 and transform: translateY(100%)
  • Browser immediately transitions to the declared final state
  • 400ms ease gives users time to notice the toast
  • Exit uses the same transition but in reverse: fast by default, or controlled via class removal

The exit remains responsive by design—remove the element or toggle a class, and the 400ms curve applies in reverse. For even snappier dismissal, override with a shorter duration on an exiting class.

See the full example in skills/emil-design-eng/SKILL.md#L19-L33.


React Integration with Progressive Enhancement

For production components that must support older browsers, the repository suggests a data-attribute bridge:

function Toast({ visible, children }) {
  return (
    <div
      className="toast"
      data-mounted={visible}
    >
      {children}
    </div>
  );
}
/* Fallback: JS-driven state */
.toast[data-mounted="false"] {
  opacity: 0;
  transform: translateY(100%);
  transition: 
    opacity 150ms ease-out, 
    transform 150ms ease-out;
}

.toast[data-mounted="true"] {
  opacity: 1;
  transform: translateY(0);
  transition: 
    opacity 400ms ease, 
    transform 400ms ease;
}

/* Progressive enhancement: native @starting-style when supported */
@supports (@starting-style) {
  .toast[data-mounted="true"] {
    /* Remove explicit entry transition—let @starting-style handle it */
    transition: 
      opacity 150ms ease-out, 
      transform 150ms ease-out;
  }
}

This layering ensures fast exits everywhere and deliberate entries where supported, with JavaScript providing a baseline experience.


State Machine for Complex Interactions

Multi-state components benefit from explicit duration tokens:

:root {
  --duration-responsive: 150ms;
  --duration-deliberate: 1200ms;
  --easing-exit: cubic-bezier(0, 0, 0.2, 1);
  --easing-enter: linear;
}

.held-action {
  /* Base: responsive */
  transition: 
    var(--property, all) 
    var(--duration-responsive) 
    var(--easing-exit);
}

.held-action.is-pressing {
  /* Pressed: deliberate */
  transition-duration: var(--duration-deliberate);
  transition-timing-function: var(--easing-enter);
}

.held-action.is-releasing {
  /* Explicit fast cleanup */
  transition-duration: calc(var(--duration-responsive) / 2);
}

The is-releasing class handles the edge case where the user aborts mid-hold: even faster than standard exit to signal cancellation.


Key Source Files

File Purpose
[skills/emil-design-eng/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/emil-design-eng/SKILL.md) Central definition of asymmetric timing policy, @starting-style patterns, and GPU optimization guidance
[skills/review-animations/STANDARDS.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md) Review checklist enforcing "enter slower, exit faster" in code review
[skills/prototype/SKILL.md](https://github.com/emilkowalski/skills/blob/main/skills/prototype/SKILL.md) Methodology for testing multiple timing variations during design iteration
[README.md](https://github.com/emilkowalski/skills/blob/main/README.md) Repository overview and skill installation

Summary

  • Asymmetric timing prioritizes user perception: slow enters for decision-making, fast exits for system responsiveness
  • CSS transitions enable mid-animation retargeting that keyframes cannot match, critical for interruptible press-and-hold interactions
  • :active state overrides provide the simplest implementation path, with @starting-style offering a modern pure-CSS alternative
  • GPU-accelerated properties (transform, opacity) keep animations smooth regardless of duration asymmetry
  • The emilkowalski/skills repository treats this pattern as a design engineering requirement, not a stylistic preference

Frequently Asked Questions

What makes asymmetric timing different from standard CSS transitions?

Standard transitions often use symmetric durations—300ms in, 300ms out. Asymmetric timing intentionally varies duration by direction to serve user psychology: longer enters create deliberation space, shorter exits confirm responsiveness. The CSS mechanism is identical; only the values and state mapping differ.

When should I use @starting-style versus JavaScript-driven entry?

Use @starting-style when the entry animation needs no JavaScript coordination—purely visual reveals like toasts, modals, or dropdowns. Fall back to data attributes or classes when you need synchronized state (e.g., waiting for data before revealing) or broad browser support (pre-2024 Chromium, Firefox).

How do I prevent the slow enter from feeling sluggish?

Pair longer duration with linear or subtle easing—the consistent progress signals intentionality. Avoid ease-in curves that stall at the start. The repository recommends 2s linear for deliberate actions, which feels intentional rather than broken because the visual change is continuous and clearly tied to the hold gesture.

Can this pattern work with Framer Motion or other JS animation libraries?

Yes, but you sacrifice the retargeting advantage of CSS transitions. In emilkowalski/skills, CSS is preferred specifically for interruptibility. If using JS libraries, implement animate() with composite: 'replace' and manual duration switching to approximate the same behavior—though the browser's native transition interpolation remains smoother under load.

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 →