
The linear() Function Is a Physics Engine
Stop bundling heavy spring physics libraries now that CSS can bake complex coordinate-based motion directly into the browser's engine.
Your browser's CSS engine is now a more efficient physics engine than the JavaScript libraries you’re currently importing. For years, we’ve been told that if we want "organic" motion—the kind that bounces, snaps, and feels like it has weight—we have to reach for Framer Motion, GSAP, or a custom spring-solver script. We accepted the "JS tax" because cubic-bezier simply couldn't handle the complexity of a ball bouncing or a spring oscillating. It literally didn't have enough control points.
But the linear() easing function changed the game. It isn't just a way to make things move at a constant speed anymore; it is a mechanism to bake complex, coordinate-based motion directly into the browser's compositor.
The Bezier Ceiling
Since the dawn of CSS transitions, we’ve been trapped in a world of four-point Bézier curves. The cubic-bezier(x1, y1, x2, y2) function is elegant, but it has a fundamental limitation: it can only create a single curve with two handles.
You cannot create a bounce with a single Bézier curve. You cannot create a spring that overshoots and settles with a single Bézier curve. To get those effects in pure CSS, you used to have to chain dozens of @keyframes together, manually calculating the percentages and easing for every single segment. It was a nightmare to maintain and even harder to get right.
The linear() function—specifically the new multi-point syntax—removes that ceiling. Instead of defining a curve with two handles, you define a curve with as many points as you want.
Not Your Grandfather's linear
When you hear linear, you probably think of transition-timing-function: linear;. Boring. Robotic. The new linear() function is a completely different beast. It accepts a list of values that act as a piecewise linear approximation of a curve.
/* A simple ramp */
transition-timing-function: linear(0, 1);
/* A more complex shape */
transition-timing-function: linear(0, 0.5 20%, 0.8 60%, 1);In that second example, the browser isn't just going from 0 to 1. It’s moving to 50% of the value at the 20% mark of the duration, then to 80% of the value at the 60% mark. By providing enough of these points, you can approximate any mathematical function—including physics-based ones—with near-perfect accuracy.
Baking a Spring
The term "baking" is common in 3D animation. Instead of calculating physics in real-time (which is expensive), you calculate the physics once, record the position at every frame, and play back that recording.
The linear() function allows us to "bake" physics into CSS.
Let's look at a spring. A spring is defined by mass, stiffness, and damping. In JavaScript, we usually use a loop to calculate the velocity and position for every frame (60 or 120 times per second). With linear(), we do that math once at build time (or via a small utility script) and output a string.
Here is what a spring-based "overshoot" looks like when translated into linear() syntax:
.card {
transition: transform 0.8s linear(
0, 0.007, 0.028, 0.063, 0.111, 0.171, 0.243, 0.326, 0.418, 0.518, 0.624, 0.732, 0.839, 0.941, 1.033, 1.111,
1.171, 1.211, 1.23, 1.231, 1.216, 1.188, 1.152, 1.111, 1.071, 1.033, 1.002, 0.98, 0.967, 0.963, 0.968, 0.979,
0.993, 1.006, 1.015, 1.02, 1.02, 1.017, 1.012, 1.007, 1.002, 0.999, 0.997, 0.997, 0.998, 1
);
}This looks like a mess of numbers, but to the browser, it's a treasure map. It tells the GPU exactly where the element should be at every millisecond of the transition. Because it's CSS, this animation runs on the compositor thread. If your main thread is bogged down by heavy JavaScript execution, this animation will still stay buttery smooth at 120fps.
Why This Beats JavaScript Libraries
I love tools like Framer Motion, but they come with a cost. When you use a JS physics library:
1. The Bundle Tax: You're adding 10kb-30kb of JS just to move a div.
2. The Main Thread Tax: The JS engine has to wake up, calculate the physics, and apply styles to the DOM every single frame.
3. The Complexity Tax: You often have to wrap your components in specific providers or use "magic" components (like motion.div).
With linear(), the "engine" is already inside the browser. It's written in C++. It’s optimized to the teeth. By moving the physics from JS to CSS, you’re essentially outsourcing the hard work to the browser's internal systems.
Practical Example: The "Bounce"
Let’s say you want a notification toast to drop from the top of the screen and bounce. Traditionally, this required an external library. Now, we can generate a bounce curve.
Here’s a practical implementation. I’ve used a simplified version of a bounce curve that you can drop into any project:
:root {
--bounce-easing: linear(
0, 0.0039, 0.0157, 0.0352, 0.0625, 0.0976, 0.1407, 0.1914, 0.2499, 0.3164, 0.3906, 0.4726, 0.5625, 0.6601, 0.7656, 0.8789, 1,
0.8507, 0.7773, 0.7513, 0.75, 0.7511, 0.7761, 0.8473, 0.9922,
0.9609, 0.9414, 0.9381, 0.9375, 0.938, 0.9413, 0.9607, 1
);
}
.toast {
animation: slide-in 0.8s var(--bounce-easing) forwards;
}
@keyframes slide-in {
from { transform: translateY(-100px); }
to { transform: translateY(20px); }
}This easing function mimics a ball hitting the floor and bouncing twice. The first section (0 to 1) is the fall. The second section (1 back down to 0.75 and back to 1) is the first bounce. The third is the final micro-bounce.
Generating These Curves
You shouldn't write these long strings of numbers by hand. That would be madness. Instead, you can use a small utility function to generate them, or use tools like Jake Archibald's Linear Easing Generator.
If you want to generate a spring curve programmatically (perhaps during a build step or in a small utility file), the logic looks like this:
function createSpringEasing(stiffness = 100, damping = 10, mass = 1) {
const points = 100;
const easingValues = [];
for (let i = 0; i <= points; i++) {
const t = i / points;
// This is a simplified spring solver (damped harmonic oscillator)
const value = solveSpring(t, stiffness, damping, mass);
easingValues.push(value.toFixed(4));
}
return `linear(${easingValues.join(', ')})`;
}
// Result: "linear(0.0000, 0.0512, 0.1241, ... 1.0000)"By generating these once, you can save them as CSS variables in your design system. var(--ease-spring-heavy), var(--ease-bounce), var(--ease-squish). Your UI feels alive, but your JS bundle stays dead quiet.
The Gotchas and Limitations
It wouldn't be a technical post without some reality checks.
1. Browser Support
As of 2024, linear() is supported in all major evergreen browsers (Chrome 113+, Firefox 112+, Safari 17.2+). However, if you have to support older enterprise browsers, you'll need a fallback.
.element {
/* Fallback for old browsers */
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
/* Progressive enhancement */
transition-timing-function: linear(0, 0.1, 0.5, 0.9, 1);
}2. Duration is Fixed
In a real JS physics engine, the duration is often dynamic. If you pull a spring further, it takes longer to settle. With CSS linear(), the duration is fixed in your CSS (e.g., 0.5s).
If you need a "true" spring where the duration is calculated based on distance, you'll still need a tiny bit of JS to calculate the transition-duration property based on the distance the element is moving. But even then, you're only setting a single CSS property once, rather than running a loop for the entire animation.
3. Complexity vs. Precision
The more points you add to linear(), the more accurate the physics. However, there is a point of diminishing returns. 50 to 100 points is usually enough for a perfect-looking spring. If you add 10,000 points, you're just bloating your CSS file for no perceptible visual gain.
A New Mental Model for Web Animation
We need to stop thinking about CSS as "the simple way" and JS as "the powerful way."
The introduction of linear() signifies a shift where the CSS engine is becoming a low-level API for complex motion. When you use linear(), you are providing the browser with a raw data array of positions. You are essentially writing a vertex shader for UI timing.
The next time you're about to npm install an animation library, ask yourself: *Could I just bake this?* If the animation is a standard UI interaction—a menu sliding out, a button clicking, a card expanding—the answer is almost certainly yes.
The "Physics Engine" isn't a library you download; it's already running in the background of the tab you're reading right now. You just need to give it the right coordinates.


