# Enter and Exit Along the Same Path

**MUST** · **ID:** `animations-symmetric-exit-path` · **Category:** animations
**Source:** [Emil Kowalski](https://emilkowalski.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-symmetric-exit-path

> MUST: Exit along the path the element entered on — in-from-the-right means out-to-the-right — with the easing mirrored via inverse cubic-bezier control points: enter `(x1, y1, x2, y2)` → exit `(1 - x2, 1 - y2, 1 - x1, 1 - y1)`. This is DIRECTION and curve shape, orthogonal to `animations-emil-asymmetric` (DURATION — exits snap); a correct panel obeys both: back out the right edge, on the mirrored curve, faster than it came in.

A panel that slides in from the right must dismiss to the right, with the easing mirrored

Motion is how an interface teaches spatial layout: the enter animation is a claim about where the panel LIVES when it is not on screen. Slide it in from the right and the user now believes it is parked off the right edge — so dismissing it downward silently revokes that, and they are left with no model of where the thing went or how to get it back. The exit must retrace the enter. Mirroring the easing is the same argument one level down: a cubic-bezier is a shape, and reversing endpoints does not reverse the shape. Take the enter's (x1, y1, x2, y2) and use its inverse control points — (1 - x2, 1 - y2, 1 - x1, 1 - y1) — for the return, so an ease-out entrance leaves on the ease-in that is literally its mirror image. IMPORTANT — this does NOT contradict animations-emil-asymmetric, and the two are constantly confused. That rule is about DURATION (deliberate actions take their time; exits snap). This one is about DIRECTION and CURVE SHAPE. They are orthogonal, and the correct panel obeys both: it exits back out the right edge, on the mirrored curve, in less time than it took to come in. Nor is this animations-correct-transform-origin, which fixes where a scale ORIGINATES — a popover can have a perfectly anchored origin and still be wrong because it travels out an edge it never came in through.

## Rule snippet

```tsx
/* enter */ transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
/* exit  */ transition-timing-function: cubic-bezier(0.7, 0, 0.84, 0);
```

## Bad — do not do this

`animations-symmetric-exit-path-bad`

```tsx
import { useEffect, useRef, useState } from 'react';

/**
 * Bad: the panel slides IN from the right and OUT through the bottom.
 *
 * The enter tells the user "I live off the right edge"; the exit then throws it
 * somewhere else entirely. The spatial model the enter just built is destroyed by
 * the exit, and the next time they want it back they have no idea where to reach.
 */

const OFF_RIGHT = 'translate3d(110%, 0, 0)';
const OFF_BOTTOM = 'translate3d(0, 110%, 0)';

export function SymmetricExitPathBad() {
  const [open, setOpen] = useState(false);
  const [resting, setResting] = useState(OFF_RIGHT);
  const [animating, setAnimating] = useState(false);
  const raf = useRef(0);

  useEffect(() => () => cancelAnimationFrame(raf.current), []);

  const openPanel = () => {
    // Remounted off the RIGHT edge with no transition, then slid in.
    setAnimating(false);
    setResting(OFF_RIGHT);
    raf.current = requestAnimationFrame(() =>
      requestAnimationFrame(() => {
        setAnimating(true);
        setOpen(true);
      })
    );
  };

  const closePanel = () => {
    // ...and dismissed DOWNWARD. Different axis, different edge, no relationship
    // to the way it arrived.
    setAnimating(true);
    setResting(OFF_BOTTOM);
    setOpen(false);
  };

  return (
    <div className="w-full max-w-sm space-y-3">
      <button
        onClick={open ? closePanel : openPanel}
        className="rounded-lg bg-primary px-4 py-2 text-sm text-primary-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        {open ? 'Dismiss panel' : 'Open panel'}
      </button>

      <div className="relative h-40 w-full overflow-hidden rounded-lg border border-border bg-muted">
        <div className="p-3 text-xs text-muted-foreground">Page content</div>

        <div
          aria-hidden={!open}
          className="absolute inset-y-0 right-0 w-40 border-l border-border bg-card p-3 text-sm text-card-foreground"
          style={{
            transform: open ? 'translate3d(0, 0, 0)' : resting,
            transition: animating ? 'transform 320ms ease-out' : 'none',
          }}
        >
          <p className="font-medium">Details</p>
          <p className="mt-1 text-xs text-muted-foreground">
            In from the right, out through the floor.
          </p>
        </div>
      </div>

      <p className="text-xs text-destructive">
        Enter travels along X, exit travels along Y. The panel does not go back where it came
        from, so the user loses track of where it went.
      </p>
    </div>
  );
}
```

## Good — do this

`animations-symmetric-exit-path-good`

```tsx
import { useEffect, useRef, useState } from 'react';
import { useSimulatedReducedMotion } from '@/hooks/useSimulatedReducedMotion';

/**
 * Good: in from the right, out to the right — the same path, reversed.
 *
 * The easings are mirrors of each other too. Enter uses cubic-bezier(0.16, 1, 0.3, 1);
 * the exit uses its inverse control points — (1 - x2, 1 - y2, 1 - x1, 1 - y1) —
 * i.e. cubic-bezier(0.7, 0, 0.84, 0), so the return retraces the outbound curve
 * rather than merely reversing the endpoints.
 */

const ENTER_EASE = 'cubic-bezier(0.16, 1, 0.3, 1)';
const EXIT_EASE = 'cubic-bezier(0.7, 0, 0.84, 0)';
const OFF_RIGHT = 'translate3d(110%, 0, 0)';

export function SymmetricExitPathGood() {
  const [open, setOpen] = useState(false);
  const [animating, setAnimating] = useState(false);
  const raf = useRef(0);
  const reduced = useSimulatedReducedMotion();

  useEffect(() => () => cancelAnimationFrame(raf.current), []);

  const toggle = () => {
    setAnimating(true);
    raf.current = requestAnimationFrame(() => setOpen((v) => !v));
  };

  const transition = reduced
    ? 'opacity 150ms linear'
    : `transform 320ms ${open ? ENTER_EASE : EXIT_EASE}`;

  return (
    <div className="w-full max-w-sm space-y-3">
      <button
        onClick={toggle}
        className="rounded-lg bg-primary px-4 py-2 text-sm text-primary-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
      >
        {open ? 'Dismiss panel' : 'Open panel'}
      </button>

      <div className="relative h-40 w-full overflow-hidden rounded-lg border border-border bg-muted">
        <div className="p-3 text-xs text-muted-foreground">Page content</div>

        <div
          aria-hidden={!open}
          className="absolute inset-y-0 right-0 w-40 border-l border-border bg-card p-3 text-sm text-card-foreground"
          style={{
            // Reduced motion: the travel is dropped, the crossfade still reports the
            // state change. It never exits along a path it did not enter on.
            transform: reduced ? 'none' : open ? 'translate3d(0, 0, 0)' : OFF_RIGHT,
            opacity: reduced && !open ? 0 : 1,
            transition: animating ? transition : 'none',
          }}
        >
          <p className="font-medium">Details</p>
          <p className="mt-1 text-xs text-muted-foreground">
            In from the right, out to the right.
          </p>
        </div>
      </div>

      <p className="text-xs text-success">
        {reduced
          ? 'Reduced motion: no travel at all — but the panel still never leaves by an edge it did not arrive from.'
          : 'Same axis, same edge, mirrored easing. The exit is the enter played backwards, so the panel stays where the user left it.'}
      </p>
    </div>
  );
}
```

## References

- [Emil Kowalski — apple-design SKILL.md](https://raw.githubusercontent.com/emilkowalski/skills/main/skills/apple-design/SKILL.md)
- [Apple — Designing Fluid Interfaces (WWDC 2018)](https://developer.apple.com/videos/play/wwdc2018/803/)
- [MDN — cubic-bezier() easing function](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/easing-function/cubic-bezier)
