Craft

Easing is a personality

Two buttons can have the same colour, the same size and the same 300 ms duration and still feel completely different. The difference is the curve: how the value gets from start to end. Easing decides whether an interface feels brisk, calm, heavy or toy-like, and it is one of the cheapest places to be deliberate. The curve definitions below come from MDN and the CSS Easing specification. The durations and the choice of curve for each job are working conventions, and the text says where one is a convention.

What a curve actually describes

An easing function maps time to progress. Both run from 0 to 1. Linear progress means the value changes at a constant rate, which is how almost nothing in the physical world moves, and why linear motion looks mechanical. Every other curve says how much of the change happens early and how much late.

CSS gives you five named curves, plus step functions and functions for custom ones. The named curves are shortcuts for cubic Bezier definitions, and MDN lists them like this:

KeywordEquivalentCharacter
linearcubic-bezier(0, 0, 1, 1)Constant speed. Right for spinners and progress, wrong for almost everything else.
easecubic-bezier(0.25, 0.1, 0.25, 1)The default. Quick start, long gentle finish.
ease-incubic-bezier(0.42, 0, 1, 1)Slow start, fast end.
ease-outcubic-bezier(0, 0, 0.58, 1)Fast start, slow end.
ease-in-outcubic-bezier(0.42, 0, 0.58, 1)Slow start, fast middle, slow end.

A cubic-bezier(x1, y1, x2, y2) curve is drawn between the points (0,0) and (1,1) with two control points you place. Drag the first control point up and left and the motion front-loads; drag the second one down and right and it saves the change for the end. The specification requires x1 and x2 to be within 0 to 1 and treats the definition as invalid otherwise, because time cannot go backwards. The y values have no such limit, which is how overshoot works: a y2 above 1 pushes progress past the target before it returns.

Reading a curve in numbers

A graph hides what the eye cares about, which is how much has happened after a given slice of time. Solving each curve for progress at fixed fractions of the duration gives this:

Curve10% of time1/350%2/390%
ease-out0.1610.4880.6850.8440.983
ease-in0.0170.1560.3150.5120.839
ease-in-out0.0200.2320.5000.7680.980
cubic-bezier(0.22, 1, 0.36, 1)0.4010.8670.9610.9921.000

Take a 300 ms animation moving 100 px. With ease-out, 48.8 px has moved after 100 ms and the final 100 ms still covers 15.6 px, so the end of the move is gentle but not frozen. With the steeper curve in the last row, 86.7 px has moved after 100 ms and the last 100 ms covers under 1 px, so the element looks finished well before the clock says so. The ease-in row explains why it feels slow as an entrance: after the first 100 ms only about 16 percent of the distance is covered.

The rule of thumb: arrive with ease-out, leave with ease-in

Motion that responds to the user should start moving immediately, because a slow start is easily read as lag. That is the argument for ease-out on entrances, a convention in many design systems: the element appears at its fastest and decelerates into place, like something you set down on a table.

Exits work the other way. An element leaving the screen is already doing its job, so it can take a moment to build up speed and then be gone. Ease-in on exits feels like something being picked up and thrown clear. Because the end of an ease-in is its fastest point, the element leaves at speed instead of crawling off.

Two corollaries follow:

  • Exits should be shorter than entrances. The user has already decided to dismiss something, so a long exit is a tax. A workable split is roughly two thirds of the entrance duration.
  • Ease-in-out is for movement between two visible states. A tab indicator sliding from one tab to the next, a card reordering in a list, a sheet moving between half and full height. Its slow start is fine when the object is already on screen and nothing is waiting for it.

Plain ease-in should rarely be used for an entrance. The slow beginning makes the interface feel unresponsive, and the fast ending makes the element hit its position at full speed.

Why the built-in curves feel weak

The keyword curves are gentle, as the numbers above show: ease-out has covered less than half the distance (0.488) after a third of the time. For interface work, a steeper ease-out often looks and feels sharper. More of the change happens early and the settle is a longer, softer tail.

These are widely used power-law easings, all valid cubic-bezier() values:

:root {
  --ease-out-soft: cubic-bezier(0.33, 1, 0.68, 1);   /* about a cubic ease-out */
  --ease-out-firm: cubic-bezier(0.22, 1, 0.36, 1);   /* about a quintic ease-out */
  --ease-out-snap: cubic-bezier(0.16, 1, 0.3, 1);    /* about an exponential ease-out */
  --ease-in-soft:  cubic-bezier(0.32, 0, 0.67, 0);   /* about a cubic ease-in */
  --ease-in-out:   cubic-bezier(0.65, 0, 0.35, 1);   /* about a cubic ease-in-out */
}

The names in the comments refer to the power law of the function each Bezier approximates: the higher the power, the more front-loaded the motion. A cubic Bezier cannot reproduce a power curve exactly. Comparing each of these against its target formula (1 - (1 - t)^3, 1 - (1 - t)^5, 1 - 2^(-10t), t^3 and the cubic ease-in-out) over 1,000 sample points shows a largest gap of 0.003 for the cubic pair, 0.010 for the ease-in-out and about 0.011 to 0.012 for the quintic and exponential ones. Those gaps are invisible in motion. The time each curve takes to reach 90 percent of the change is 74 percent of the duration for ease-out, 54 percent for the soft curve, 37 percent for the firm one and 33 percent for the snap one.

Duration is the other half

A good curve at the wrong duration still feels wrong. No standard sets durations: the bands below are working guidance drawn from common practice, so tune them on your own devices. Use bands, not single numbers:

PurposeDurationCurveNotes
Hover, press, toggle feedback100-200 msease-out-softSlower than about 200 ms can feel as if the control is thinking.
Small reveals: tooltip, menu, dropdown150-250 msease-out-softExit about 100-150 ms with ease-in-soft.
Panels, drawers, modals entering250-400 msease-out-firmLarger distance can take the upper end.
Panels, drawers, modals leaving180-280 msease-in-softShorter than the entrance.
Element moving between two on-screen states250-500 msease-in-outTab indicator, reorder, shared-element moves.
Page-level transitions400-600 msease-out-snapKeep content readable before the animation ends.
Story or hero moments700 ms and aboveease-out-snapRare, one per screen at most, and skippable.

Distance matters too. A tooltip moving 8 px and a drawer moving 360 px should not share a duration, so let distance move you within a band. A square-root rule is a tempting model (doubling the travel adds about 41 percent to the time), but it breaks over wide ranges: 150 ms for 8 px would become about 1,000 ms for 360 px, far outside any sensible band. Use it only to choose between neighbouring values.

Stagger has the same limit. When a list reveals item by item, a gap of 30-60 ms between items reads as a wave in common practice. For twelve items, 40 ms puts the last start at 440 ms, while 100 ms puts it at 1,100 ms, long after the reader has finished looking at the first row.

Springs and overshoot

A spring is not a curve with a name, it is a physical simulation: a mass on a spring with damping. Its movement depends on current velocity, which is why springs feel natural when a gesture is interrupted: the element carries its momentum into the new target. A fixed CSS curve cannot do that.

CSS can still approximate one. MDN describes linear() as an easing function that interpolates linearly between the points you give it, typically many points to approximate a complex curve. Percentages place each point in time, and with none given the points are spread evenly:

.chip {
  /* a hand-drawn approximation of a lightly damped spring, for illustration */
  transition: transform 500ms
    linear(0, 0.55 14%, 1.06 36%, 0.98 52%, 1.005 68%, 1);
}

Values above 1 are overshoot. Here progress peaks at 1.06 after 36 percent of the time, 180 ms of the 500 ms, and has settled to within half a percent by 68 percent (340 ms). Six points joined by straight lines will show corners on a large move, so generate real spring samples with a tool instead of writing them by hand, and remember that this is a recording: it always plays the same way. For interruptible motion, a small JavaScript spring solver or a library is the right tool. MDN marks linear() as widely available across current browsers.

Use overshoot sparingly. A 5 percent overshoot on a confirmation chip reads as friendly, while the same overshoot on a dialog every time it opens reads as a toy. A calm brand wants almost none.

Matching curve to character

You can choose a personality and then derive the numbers from it:

  • Precise and quiet: short durations in the lower half of each band, --ease-out-firm, no overshoot, small distances. Feels like a well-made instrument.
  • Warm and friendly: mid durations, --ease-out-soft, a little overshoot on confirmations only.
  • Cinematic: long durations on a few hero moments with --ease-out-snap, stillness everywhere else. The contrast between stillness and the one big move creates the effect.
  • Playful: springs everywhere, visible overshoot, slightly longer durations. Hard to keep readable in dense interfaces.

Pick one, write it down as tokens, and apply it everywhere. Mixed personalities are a common reason a product feels uneven even when each animation is individually nice.

Worked example: a dropdown

Take a fictional account menu. The goal is "quiet and precise".

.menu {
  transform-origin: top right;
  opacity: 0;
  transform: translateY(-6px) scale(0.98);
  transition: opacity 120ms var(--ease-in-soft),
              transform 120ms var(--ease-in-soft);
  pointer-events: none;
}
.menu[data-open] {
  opacity: 1;
  transform: none;
  transition: opacity 180ms var(--ease-out-firm),
              transform 180ms var(--ease-out-firm);
  pointer-events: auto;
}

The entrance is 180 ms with a firm ease-out and the exit is 120 ms with an ease-in. Using the firm curve's numbers, the entrance is about 40 percent complete after 18 ms and 86 percent complete after 60 ms, so the menu is nearly in place before the pointer has moved. The movement is 6 px with a 2 percent scale, which is enough to read as motion without drawing attention. Only opacity and transform change. web.dev's guidance is that these are the properties a compositor can handle without layout or paint, provided the element is on its own layer. The transition is declared on the state being entered, so each direction gets its own curve and duration.

When a transition is interrupted

Transitions are not always allowed to finish. A user opens the menu and closes it 60 ms later. The CSS Transitions specification handles a reversal like this specially: each running transition carries a reversing shortening factor, computed from how far the first transition got, and the reversing transition's duration is scaled by it, so the exit does not replay its full length from a point that is already partly done. The curve still applies to the shortened run. Test the case by hand anyway: open and close rapidly, then let the pointer leave mid-animation. If the result looks sticky, shorten the exit or use a gentler entrance curve. A spring solver handles interruption by design because it keeps the current velocity, which is the practical reason to reach for JavaScript for gesture-driven motion.

Motion has to be switchable

Some people get dizzy or sick from large movement on screen, and operating systems have a setting for it. MDN notes that scaling or panning large objects can be vestibular motion triggers. Honour the setting by reducing travel distance, dropping parallax and replacing large moves with fades. WCAG 2.2 success criterion 2.3.3, Animation from Interactions, is a Level AAA criterion that asks that motion animation triggered by interaction can be disabled unless it is essential. The full treatment is in prefers-reduced-motion, done properly. Scroll-linked motion has its own problems, covered in smooth scrolling without the regret, and route-level motion is worked through in how a page transition works without a framework.

Checklist

  • Define three to five curve tokens and three duration tokens once; never type raw cubic-bezier() values in components.
  • Entrances use ease-out, exits use ease-in and run about a third shorter.
  • Feedback stays within 100-200 ms and nothing outside a story moment goes past 700 ms, as working bands you have tested yourself.
  • Animate opacity and transform where you can, and test every animation with reduced motion switched on.
  • Judge a curve by its progress at 10 and 33 percent of the duration on a real device, not by the shape of its graph.

Questions and answers

What is the difference between ease-out and ease-in-out?

Ease-out starts at full speed and slows into the end, so the interface reacts at once. Ease-in-out also starts slowly (cubic-bezier(0.42, 0, 0.58, 1) has only 2 percent of the change done after the first tenth of the duration), which reads as a delay when the user is waiting. Use it for things already on screen that move between two positions.

Can a cubic-bezier curve overshoot?

Yes. The CSS Easing specification requires both x values to be between 0 and 1, or the definition is invalid, but the y values are free. A y above 1 makes the animated value pass its target and come back, which reads as a small bounce.

How long should a UI animation last?

No standard sets durations. A common working band is 100-200 ms for feedback such as a hover or press and 250-500 ms for panels and page elements, with anything beyond about 700 ms kept for rare, skippable story moments. Treat those as starting points and judge them on a real device.

Are CSS springs possible without JavaScript?

Approximately. The linear() easing function joins a list of progress points with straight segments, so a sampled spring can be written as a curve. It is a fixed recording of a spring, not a live simulation, so it cannot carry velocity into an interrupted gesture.

Sources

  1. MDN: CSS easing-function developer.mozilla.org
  2. MDN: linear() easing function developer.mozilla.org
  3. W3C: CSS Easing Functions Level 1 w3.org
  4. WCAG 2.2: Animation from Interactions w3.org
  5. web.dev: Stick to compositor-only properties web.dev