Entry vs Exit Easing: Different Curves for Arriving and Leaving
A dropdown opens and closes with the same transition: 0.3s ease. It works, but it feels slightly off in both directions: opening is a touch slow to start, and closing lingers when you have already moved on. The fix is not a better single curve but two different ones. Motion that brings something into view has different goals from motion that takes something out of view, and CSS lets each direction have its own curve and duration. This page explains the reasoning, shows the one cascade rule that makes asymmetric transitions work, and applies it to menus, toasts and dialogs. It belongs to Easing & Motion Design in the CSS-Only Micro-Interactions & Animations guide.
Why the two directions differ
When an element enters, the user has just asked for it. Two things matter: that the response feels immediate, and that the element lands where the eye can find it. A curve that is fast at the start and slow at the end — ease-out — delivers both. The element is visibly moving from the first frame, so the interface feels responsive, and it decelerates into its final position, which gives the eye time to track it and settle.
When an element leaves, the user is done with it. The priorities flip: the departure should not startle, and it should clear the stage quickly. A curve that starts slowly and accelerates — ease-in — begins gently and then gets out of the way. Played in reverse, an entrance ease-out is an ease-in, which is why a symmetric transition on a toggling class produces this pairing automatically; the problem is only the duration, and when you want sharper curves or a shorter exit.
The exit is also shorter. Leaving elements hold no information the user still needs, so every millisecond they remain half-visible is a millisecond of clutter. A common ratio is an exit at 60 to 80 percent of the entrance duration.
The complete implementation
The menu below opens with an ease-out over 250 milliseconds and closes with an ease-in over 160 milliseconds. The asymmetry comes entirely from where the transition declarations sit.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Entry vs exit easing</title>
<style>
:root {
--ease-enter: cubic-bezier(0.22, 1, 0.36, 1); /* strong ease-out */
--ease-exit: cubic-bezier(0.64, 0, 0.78, 0); /* strong ease-in */
--enter-duration: 250ms;
--exit-duration: 160ms;
}
body { font: 15px/1.5 system-ui, sans-serif; margin: 1.5rem; }
.menu { position: relative; display: inline-block; }
.menu__list {
position: absolute;
top: calc(100% + 0.5rem);
left: 0;
min-width: 12rem;
margin: 0;
padding: 0.5rem;
list-style: none;
border: 1px solid #cbd5e1;
border-radius: 10px;
background: #ffffff;
box-shadow: 0 10px 30px rgb(15 23 42 / 0.15);
/* CLOSED state. These transition values govern the move INTO this
state, i.e. the closing animation: ease-in, short. visibility is
delayed until the fade finishes so the menu stays visible while
leaving, then becomes non-interactive. */
opacity: 0;
translate: 0 -0.5rem;
visibility: hidden;
transition:
opacity var(--exit-duration) var(--ease-exit),
translate var(--exit-duration) var(--ease-exit),
visibility 0s linear var(--exit-duration);
}
/* OPEN state. These transition values govern the move INTO the open
state: ease-out, longer, and visibility switches immediately. */
.menu:focus-within .menu__list,
.menu:hover .menu__list {
opacity: 1;
translate: 0 0;
visibility: visible;
transition:
opacity var(--enter-duration) var(--ease-enter),
translate var(--enter-duration) var(--ease-enter),
visibility 0s linear 0s;
}
.menu__list a { display: block; padding: 0.4rem 0.6rem; border-radius: 6px; color: inherit; text-decoration: none; }
.menu__list a:hover, .menu__list a:focus-visible { background: #eef2ff; }
.menu__button { padding: 0.5rem 0.9rem; border: 1px solid #94a3b8; border-radius: 8px; background: transparent; font: inherit; }
.menu__button:focus-visible, .menu__list a:focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
.menu__list, .menu:focus-within .menu__list, .menu:hover .menu__list {
translate: 0 0;
transition-duration: 1ms;
}
}
</style>
</head>
<body>
<div class="menu">
<button class="menu__button" type="button" aria-haspopup="true">Account</button>
<ul class="menu__list">
<li><a href="/profile/">Profile</a></li>
<li><a href="/billing/">Billing</a></li>
<li><a href="/sign-out/">Sign out</a></li>
</ul>
</div>
</body>
</html>
The menu uses :hover and :focus-within so it opens for pointer and keyboard users without script. A production menu button should also toggle aria-expanded and support Escape, which needs a few lines of script or the native popover attribute; the easing principles are the same either way.
The key technique: the target state owns the transition
The rule that makes asymmetry possible is simple but rarely stated: a transition uses the transition-* values in effect on the state being entered. When a class, pseudo-class or attribute changes, the browser computes the new style — including its transition declarations — and uses those to animate from the old values to the new ones.
Once that rule is clear, several tricks follow. The visibility delay trick works because the closed state delays visibility by the exit duration, keeping the menu visible while it fades, while the open state switches visibility with no delay so the menu is interactive immediately. Interrupted transitions behave sensibly too: if the user hovers away mid-opening, the closing transition starts from the current position with the closing curve. For elements that go to and from display: none, the same asymmetry can be combined with transitioning display with allow-discrete and @starting-style entry animations.
Keyframe animations need two animations
Transitions get asymmetry almost for free because each state owns its timing. Keyframe animations do not work that way: an animation runs from its first keyframe to its last with one timing function, and when the triggering class is removed the animation simply stops — there is no automatic reverse. Elements that enter and leave with @keyframes therefore need two separate animations, one for each direction.
.toast[data-state="open"] {
animation: toast-in 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.toast[data-state="closing"] {
animation: toast-out 170ms cubic-bezier(0.64, 0, 0.78, 0) both;
}
@keyframes toast-in {
from { opacity: 0; translate: 0 1rem; }
}
@keyframes toast-out {
to { opacity: 0; translate: 0 0.5rem; }
}
The two keyframe blocks also let the paths differ. The toast enters from a full rem below but leaves by dropping only half a rem, because an exit that retraces the entrance exactly draws as much attention as the entrance did. Small differences like this are what make an interface's motion feel considered rather than mechanical.
The cost of the keyframe approach is a script-controlled intermediate state — closing — which must be set before the element is removed and then cleared when the animationend event fires. Where that script is unwelcome, transitions with @starting-style and allow-discrete achieve the same asymmetry declaratively.
Patterns for common components
- Menus and dropdowns: enter with ease-out at 200–250ms, exit with ease-in at 120–160ms, and include a small
translatetoward the trigger so the menu appears to come from it. - Toasts and notifications: enter with ease-out and perhaps a subtle overshoot from cubic-bezier overshoot, since arrival is the moment to notice them; exit with a quick ease-in fade. Auto-dismissing toasts need enough on-screen time to be read, which is a separate concern from their motion.
- Dialogs: enter with ease-out scale and fade at 250–300ms; exit with a faster ease-in fade, often without the scale, so the dialog simply dissolves when dismissed.
- Tooltips: enter after a short delay to avoid flicker when the pointer passes over, then a quick ease-out; exit immediately or with a very short ease-in. The delay pattern is covered in Hover Intent Delays With transition-delay.
- In-place changes such as toggles and tabs: both ends stay on screen, so use the same gentle ease-out or ease-in-out in both directions.
Browser support
Per-state transition declarations, custom properties in transitions, and the visibility delay technique work in every browser with CSS transitions; the transition shorthand dates from Chrome 26, Edge 12, Firefox 16 and Safari 9. The standalone translate property is supported in Chrome and Edge 104+, Firefox 72+ and Safari 14.1+. :focus-within is supported in Chrome 60+, Edge 79+, Firefox 52+ and Safari 10.1+.
FAQ
Why should entrances use ease-out? An entering element should respond immediately to the user's action and then settle gently into place. Ease-out starts fast and slows at the end, so the element is visibly moving from the first frame and comes to rest softly where the eye can land on it.
Why should exits use ease-in? A leaving element no longer needs attention. Ease-in starts slowly and accelerates away, so the element begins departing without a jolt and then clears the screen quickly. The fast end also means less time with a half-visible element in the way.
How do I set different timing for the two directions of a CSS transition? Declare the transition on both states. When the element enters a state, the transition values declared on that state apply. So the transition on the open state controls the opening, and the transition on the base state controls the closing.
Do these rules apply to elements that just change in place? Less strictly. For something that stays on screen and changes, such as a toggle moving between positions, a symmetric ease-in-out or a gentle ease-out in both directions is usually best, because both ends of the motion are on screen and matter equally.
Related
- Easing & Motion Design — the parent guide.
- Choosing Animation Durations — sizing the entry and exit durations.
- CSS Transition Timing Functions — the curves themselves.
- Anchor-Positioned Tooltips — positioning the overlays these curves animate.
Related articles
More pages in the same section.