Easing

The CSS linear() easing function: springs and bounces without JavaScript

How CSS linear() works, how it approximates spring and bounce easing, a hand-written bounce, a JS function that turns a spring into linear(), and fallbacks.

· 6 min read

For years, CSS easing meant cubic Bezier curves. They cover ease-in, ease-out and a single overshoot, but they cannot draw a spring that wobbles and settles, or a ball that bounces three times. Those needed JavaScript.

The linear() easing function changes that. It takes a list of points and connects them with straight lines, and with enough points it can trace any curve you can compute. This guide explains the syntax, writes a bounce by hand, generates a spring with a short JavaScript function, and shows how to fall back safely.

ease-out
linear() spring
linear() bounce
ease-out vs. linear() spring and bounce

The top ball uses a plain ease-out. The middle one uses a spring that overshoots by about 10% and settles, and the bottom one bounces three times against its end point. The last two are CSS transitions with no animation library involved; the script only toggles a class. The track leaves room past the end so the spring's overshoot stays visible.

How linear() works

An easing function maps time (0 to 1) to progress (0 to 1, or beyond). linear() defines that mapping as a list of stops. Each stop is an output value, optionally followed by one or two input positions as percentages:

/* three stops, spaced evenly in time: 0%, 50%, 100% */
linear(0, 0.25, 1)

/* the same 0.25 stop, moved to 75% of the duration */
linear(0, 0.25 75%, 1)

/* two percentages: hold 0.5 from 40% to 60% */
linear(0, 0.5 40% 60%, 1)

The rules are short:

Note the difference from the keyword. linear on its own means constant speed and is the same as linear(0, 1). The function only becomes interesting with more points.

Browser support

linear() works in all current major browsers: Chrome and Edge 113 and later, Firefox 112 and later, and Safari 17.2 and later. It works anywhere an easing function is accepted: transition-timing-function, animation-timing-function, the shorthands, per-keyframe timing functions, and the easing option of the Web Animations API.

If you still need to serve older browsers, see the fallback section below. An unsupported linear() makes the whole declaration invalid, so without a fallback those browsers get the default ease.

A bounce written by hand

A ball dropped onto the floor falls with increasing speed, hits, rebounds to a lower height, and repeats. In progress terms, it reaches 1 (the floor) several times, and between hits it rises back to a peak and falls again. Each fall is a parabola: starting from rest, the distance covered grows with the square of time, so at a quarter, half and three quarters of the first fall it has covered 1/16, 1/4 and 9/16 of the distance.

.ball {
  transition: transform 1000ms linear(
    0, 0.0625 9.1%, 0.25 18.2%, 0.5625 27.3%,  /* the drop */
    1 36.4%,                                    /* first hit */
    0.8125 45.5%, 0.75 54.5%, 0.8125 63.6%,     /* first rebound */
    1 72.7%,                                    /* second hit */
    0.9375 81.8%,                               /* small rebound */
    1 90.9%,                                    /* third hit */
    0.9844 95.5%,                               /* tiny rebound */
    1
  );
}

The hit times, 36.4%, 72.7% and 90.9%, are the touch points of the classic easeOutBounce curve, and the in-between values sit on its parabolas. With so few stops the arcs are slightly faceted, but at one second you will not see the corners. Add more stops between hits if the bounce is slow or large.

When you write curves by hand, put a stop exactly at every hit and every peak. Those are the moments where the motion changes direction, and a straight line drawn across one of them cuts the corner off.

Generate a spring with JavaScript

For springs, compute the points instead. The function below simulates a damped spring moving from 0 to 1, runs it until it settles, and samples the result at evenly spaced times. Because the samples are evenly spaced, the output needs no percentages.

function springToLinear({ stiffness = 300, damping = 20, mass = 1, points = 40 } = {}) {
  const dt = 1 / 1000; // simulate in 1 ms steps
  let x = 0, v = 0, t = 0;
  const trace = [0];

  // run until the spring has settled at 1
  while (t < 5 && (Math.abs(1 - x) > 0.001 || Math.abs(v) > 0.001)) {
    const force = -stiffness * (x - 1) - damping * v;
    v += (force / mass) * dt;
    x += v * dt;
    t += dt;
    trace.push(x);
  }

  // pick evenly spaced samples
  const stops = [];
  for (let i = 0; i <= points; i++) {
    const value = trace[Math.round((i / points) * (trace.length - 1))];
    stops.push(+value.toFixed(3));
  }
  stops[stops.length - 1] = 1;

  return { easing: `linear(${stops.join(', ')})`, duration: Math.round(t * 1000) };
}

const { easing, duration } = springToLinear();
console.log(`transition: transform ${duration}ms ${easing};`);

Run it once in Node or the browser console and paste the result into your stylesheet. With the defaults it prints the exact spring used in the demo above: 866 ms, peaking at about 1.106.

transition: transform 866ms linear(0, 0.065, 0.209, 0.397, 0.588, 0.751, 0.891, 0.993, 1.057, 1.094, 1.106, 1.103, 1.088, 1.07, 1.049, 1.03, 1.015, 1.003, 0.995, 0.991, 0.989, 0.989, 0.99, 0.992, 0.994, 0.996, 0.998, 0.999, 1, 1.001, 1.001, 1.001, 1.001, 1.001, 1.001, 1, 1, 1, 1, 1, 1);

The parameters behave like the ones in spring libraries:

The duration is not a free choice here: it is however long the spring takes to settle. You can play the curve over a different duration, but then it is a different spring, stretched or squashed in time. Note also that the last third of this spring is within 0.2% of the target. If that tail feels too long, raise the settle threshold from 0.001 to 0.005 to cut it.

Fallback with @supports

Declare a cubic-bezier version first, then override it inside a feature query. Browsers without linear() skip the block and keep the curve that came before it. This is how the demo above is written:

.lin-b .lin-run {
  /* closest single-overshoot curve */
  transition: transform 600ms cubic-bezier(.34, 1.56, .64, 1);
}

@supports (transition-timing-function: linear(0, 1)) {
  .lin-b .lin-run {
    transition: transform 866ms linear(0, 0.065, 0.209, /* ... */ 1);
  }
}

Change the duration in the fallback as well as the curve. A Bezier overshoot settles in one move, so it should be shorter than the spring it stands in for.

For people who prefer reduced motion, springs and bounces are the first thing to remove. Inside @media (prefers-reduced-motion: reduce), switch to a short ease-out or remove the transition entirely.

Where linear() falls short

A linear() spring is a recording. It always takes the same time and follows the same shape, no matter how the animation started. A JavaScript spring, such as the ones in Motion or GSAP, can start from the current velocity when an animation is interrupted, for example when a user flicks a drawer and lets go. For entrances, hovers, toggles and toasts, the recorded curve is indistinguishable. For drag and throw gestures, keep the JavaScript spring. The slide to confirm button is a good example of where a single overshoot curve is enough.

Get the curve from a real animation

If you like the spring on a particular site, you can measure it instead of tuning numbers. Finding the easing curve of an animation walks through the options. The Capture Motion extension records the keyframes, durations and easing of every animated property on the page, whether it comes from CSS, the Web Animations API, GSAP or Lottie, and gives them back as CSS, React or GSAP code, JSON or a Figma Motion timeline. For the basics of Bezier curves, start with CSS easing functions explained.

Questions

Is linear() the same as the linear keyword?

No. The keyword linear means constant speed. The function linear(...) takes a list of points and draws straight lines between them, so with enough points it can follow any curve, including springs and bounces.

Which browsers support CSS linear()?

All current major browsers: Chrome and Edge from version 113, Firefox from 112 and Safari from 17.2. Use @supports to give older browsers a cubic-bezier fallback.

How many points does a spring need?

Around 30 to 50 evenly spaced points is enough for a smooth spring at normal durations. Fewer points show visible corners in the motion; many more only make the string longer.

Does a linear() spring respond to velocity like a real spring?

No. It is a fixed curve recorded from one spring, played over a fixed duration. If the animation is interrupted mid-way, it does not carry momentum the way a JavaScript physics spring does.