The Issue with Bézier Curves
Bézier curves have long been the standard for defining easing in CSS animations, offering familiar options like ease-in, ease-out, and ease-in-out. These options significantly impact how an animation feels to the observer, even when the duration and distance traveled remain constant.
However, Bézier curves do have limitations. They cannot natively produce spring or bounce effects. Previously, achieving those effects meant pulling in JavaScript libraries like React Spring. That approach comes with trade-offs, chiefly that many JavaScript-driven animations execute on the main thread and can stutter under heavy application load.
The modern CSS solution is the linear() timing function, which enables native springs, bounces, and other complex interpolations without any JavaScript.
How linear() Works
The principle behind linear() is straightforward: instead of relying on a mathematically complex curve, you plot individual points on a Cartesian plane to define the easing path.
For example, an "ease" curve can be approximated with as few as 11 points. Visually, it looks like a curve, but it is actually a series of straight-line segments connecting those points. This "connect-the-dots" structure is the reason for the "linear" moniker.
A basic CSS snippet uses uniformly spaced progress ratios between 0 and 1:
While you can use values to overshoot the target (e.g., 1.25 indicates going 25% past), hand-crafting a list for a smooth spring effect is infeasible. A handful of points will render a robot-like motion rather than natural oscillation. You need significantly more data points for it to feel authentic.
Given the impracticality of manual generation, dedicated design tools are the recommended approach. Two are prominent:
- Linear() Easing Generator: Created by Jake Archibald and Adam Argyle, this tool pre-loads the math to convert spring parameters into an optimized
linear()string. If you have a custom JavaScript timing function, you can modify its code. - Easing Wizard: Regardlesss of your design preferences, this tool stands out as the most comprehensive for modeling springs, bounces, and wiggles, offering a huge range of testing utilities.
These tools use an advanced syntax for linear() that includes both a progress ratio and a time percentage for each point.
linear(0 0%, 0.5 50%, 1 100%)
In this advanced form, each point's timing can be asymmetrical. This allows for a more strategic placement, achieving the same effect with fewer—but more detailed—entries.
Practical Limitations
Despite its capabilities, linear() carries some issues to be aware of.
1. It Remains Time-Based
Physics-based libraries let you define springs through properties like stiffness, damping, and mass, with the animation duration derived internally from the physics. By contrast, CSS transitions require an explicit duration.
Tools address this differently. The Linear() Easing Generator automatically calculates the duration needed for the spring to settle. However, this breaks with a zero-friction spring, which would oscillate forever and demands an infinite duration that CSS can't express.
Easing Wizard handles it by treating duration as a fixed, user-configurable variable. It then internally clamps certain parameters to prevent impossible readings (e.g., low mass/stiffness negates the effect of damping). Neither approach offers a perfect match for physics-based timing.
2. Interruption Behavior
When a transition is interrupted, CSS and JS physics diverge significantly. With JS-based springs, the element's inertia is considered—it decelerates before reversing direction. CSS linear(), however, reverses instantly upon interruption, which can feel rigid and unnatural.
The cause lies in CSS's definition of the reversing shortening factor. For interrupted transitions, it can proportionally shrink the effective duration. A 1600ms spring might rerun in 400ms, a strategy that works for Bézier curves but sabotages the illusion of a natural rebound effect.
3. Performance and BundleSize
Simulating a spring convincingly requires numerous data points—sometimes upwards of 40. Tests demonstrate that complex linear() strings do not degrade frame rates, even on lower-end hardware. A <code>linear(0, 1)</code> and a string with 100+ points perform equally.
CSS bundle size, however, is worth attention. Three large springs (averaging 75 values each) added roughly 1.3kB to a real application's CSS bundle. That translates to a few milliseconds of download time on a slower connection, which is negligible.
To maximize efficiency, store shared linear() timings in CSS variables. This prevents the bloat of repeatedly embedding the lengthy strings across your stylesheet.
Managing linear() in practice
linear() output is verbose and hard to read, which makes it impractical to paste directly into stylesheets. Rather than sprinkle these strings throughout a codebase, store a few common timing functions in globally-scoped CSS variables. If you already maintain design tokens, extending them with linear() values is a natural fit.
Because linear() is relatively new, you'll need a strategy for older browsers. A practical pattern is to define fallback springs with Bézier curves, which can approximate an overshoot effect, albeit with less smoothness than a true spring. The @supports rule then lets you override the same variable with the real linear() string only in supporting browsers.
html {
--spring-smooth: cubic-bezier(...);
--spring-smooth-time: 1000ms;
@supports (animation-timing-function: linear(0, 1)) {
/* stiffness: 235, damping: 10 */
/* prettier-ignore */
--spring-smooth: linear(...);
}
}
/* Then, to use this timing function: */
@media (prefers-reduced-motion: no-preference) {
.thing {
transition:
transform var(--spring-smooth) var(--spring-smooth-time);
}
}
When you do define a spring, add a comment recording its stiffness and damping parameters so the linear() string can be regenerated later. Also, disable formatter line-splitting on that declaration; otherwise, a single-line string becomes a multi-line list that is far harder to scan.
These variables can then feed any transition or keyframe animation property. As with all motion, ensure you respect user preferences via the prefers-reduced-motion media query.



