Cubic Bezier and Easing Function Tools: Judging the CSS Output
What cubic-bezier generator tools emit versus what you should ship: auditing the verbose output, stale prefixes, and unnecessary paint costs in generated easing CSS.
The common wrong assumption about cubic bezier CSS easing tools is that the output is production-ready. Most tools, including the popular ones you find in the first page of search results, emit verbose, prefixed, and speculative code that a hand-writing developer would never ship. The truth: the tool is for designing the curve, not for writing the final stylesheet. What you paste is a starting point that needs surgical editing. Remove stale prefixes. Delete will-change where it does nothing. Compress keyframe declarations. The engine that renders your page in 2026 is not the engine those tools were written for.
What the Typical Tool Emits, and Why It Is Wrong
Paste a curve from any visual cubic-bezier editor, say the one at cubic-bezier.com or the embedded tool in a CSS framework’s docs, and you get something like this:
/* Generator output, verbatim */
.animated-element {
-webkit-transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-moz-transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-o-transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-ms-transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
will-change: transform, opacity;
-webkit-animation: slide-in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-moz-animation: slide-in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
-o-animation: slide-in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation: slide-in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
The Dead Weight of Vendor Prefixes
The first problem is the vendor prefixes. -webkit-transition was necessary for Safari 3.1 through roughly Safari 8, but every engine in the Baseline set, Chrome, Edge, Firefox, Safari, Opera, has supported unprefixed transition and animation since 2015. The -moz-, -o-, and -ms- prefixes were never needed for this property in any engine that also supported cubic-bezier(). Each prefixed line is dead code. It adds bytes and, worse, confuses anyone maintaining the file later who assumes it is there because some older client needs it. The stale prefixes are a sign the tool has not updated its output template in a decade.
The Memory Trap of `will-change`
The second problem is will-change: transform, opacity. The tool cannot know whether the element is going to be animated, when, or how often. will-change is a hint that promotes the element to its own compositor layer, which consumes memory. On a phone, that is a scarce resource. If the element is a one-off card that animates once on load, the promotion stays in place for the life of the page, holding a layer that does nothing. Apply will-change before a motion is about to start, in response to a hover or a class toggle, and remove it after the motion completes. A static stylesheet that declares it unconditionally is a waste. The paint cost of an element with a permanent layer is not the issue; the memory cost and the potential for layer explosion when many elements have it are.
The `all` Keyword and the Main Thread
The third problem is the all keyword in the transition. transition: all tells the engine to watch every animatable property for changes. When the element changes, the engine checks each property’s computed value, and any property that changes triggers its own timing function. You only want to transition transform and opacity. Those are the two properties that the compositor thread can animate without involving layout or paint. Animating anything else, left, top, width, or margin, forces the main thread to recalculate layout and repaint at every frame. No cubic-bezier curve can fix that. The CSS Triggers categorisation, which labels properties as causing layout, paint, or composite changes, is the reference. For transform, the cost is composite-only: the compositor thread handles the interpolation. For opacity, it is composite-only as well, with the caveat that a semi-transparent element over a complex background may still repaint. For width, the cost is layout plus paint, the slowest path.
The Hand-Written Equivalent, and What It Costs Differently
Write the same motion the way you would if you knew the rendering pipeline. The curve is the same, the intent is the same, but the output is a tenth of the size and does only what is needed.
/* Hand-written equivalent */
.animated-element {
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.animated-element.is-slide-in {
transform: translateX(0);
animation: slide-in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
@keyframes slide-in {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
Notice what is gone: all prefixes, the will-change, the all in the transition, and the separate prefixed animation declarations. The transition is scoped to transform, so the engine only watches that property. The keyframe uses from and to instead of a percentage list, which is shorter and idiomatic. The animation-timing-function is declared on the animation shorthand, so it applies to the whole run. If you wanted a different behaviour, say, the curve only applies to the first half, you would move the timing function into the keyframe. For a one-shot slide-in, this is complete.
Comparing Paint and Compositor Cost
Compare the paint and compositor cost. The tool output, with will-change and all, has the element permanently promoted to a layer, and any property change, even a non-animated one like a class toggle that changes colour, will compute a transition if the property is animatable. The hand-written version has no permanent layer, and only transform triggers the compositor. Inspect the element in DevTools during the motion. You will see the compositor thread handling the transform updates, and the main thread is free to handle input and script. If you had used transition: all and changed opacity simultaneously, the engine would have to paint, because opacity changes can require compositing a new frame. With transform alone, the compositor can repurpose the existing layer bitmap and just reposition it. The difference is the difference between a smooth 60fps motion on a mid-range Android phone and a janky one that drops frames whenever the main thread is busy with anything else.
Reading the Curve Without a Visual Editor
The curve itself, cubic-bezier(0.68, -0.55, 0.265, 1.55), has control point P1 at (0.68, -0.55) and P2 at (0.265, 1.55). The negative y-value on P1 means the motion overshoots the starting position before it begins, and the y-value above 1 on P2 means it overshoots the end position before settling. This is the classic “back” easing, visually useful for a playful slide-in. But note that the x-values are within the valid range [0,1]. That is a requirement for a timing function, because the x-axis is time. The y-values can be any number, because the y-axis is the progress, and negative or greater-than-one values produce the overshoot. Internalise that grammar so you can read any curve without a visual editor.
CSS Easing Function Tools: What They Are For, and What They Hide
A CSS easing function tool is for exploring the space of possible curves. You drag P1 and P2, see the curve on a graph, and copy the numbers. That is its legitimate use: it is a visual editor for the mathematics of the timing function. What it hides is the context. It cannot tell you whether the element you are moving is a large image that will need repainting, or a tiny icon that the compositor can slide cheaply. It cannot tell you whether the motion runs once or in a loop, whether it is triggered by a hover or a scroll, or whether the page is already under memory pressure on a low-end device. Those are the questions that determine whether the curve is the right choice, and no tool answers them.
Treat the tool as a sketchpad. Design the curve, copy the four numbers, then close the tool and write the CSS by hand. The tool is not a stylesheet author; it is a curve explorer. If you find yourself pasting its output directly into a build and calling it done, you are shipping the tool’s defaults, prefixes, will-change, all, as if they were your decision. The performance-conscious developer does not do that. The performance-conscious developer also knows that the curve is the least of the performance equation. The property being animated and whether it runs on the compositor thread dwarf the difference between two visually similar curves. A linear curve on transform beats a beautifully shaped cubic-bezier on width every time.
The Cubic-Bezier Visual Editor You Already Have in DevTools
The DevTools cubic-bezier editor is the one visual editor you do not need to download. In Chrome, Firefox, or Edge, open the Styles pane, find a transition-timing-function or animation-timing-function declaration, and click the little curve icon next to the value. A pop-up opens with an interactive graph. Drag the control points, and the curve updates in real time. The output is the exact cubic-bezier() string, which you can copy. Unlike a third-party tool, the DevTools editor is part of the engine you are testing in, so what you see is what the engine will compute. No version skew, no stale template, no extra sugar. Use it when you are already debugging motion and want to tweak the feel without leaving the context of the running page.
Custom Easing Curves: The Overshoot Trap
A custom easing curve is any cubic-bezier() that is not one of the keywords. The trap is overshoot. Curves with y-values below 0 or above 1 are visually striking, but they can feel wrong in a UI. A button that overshoots its target by 20% and then snaps back reads as a mistake, not as a flourish. The overshoot also extends the perceived duration: the motion appears to take longer than the specified time, because the element is still moving after the “end” of the time window. If you are moving something that must land precisely, a modal that should settle exactly at its final position, an overshooting curve can cause the element to overlap other content for a few frames. That looks broken. The fix: use overshoot only for decorative, non-essential motion, and keep the overshoot amount small, under 10% of the total distance.
Choosing the Right Timing Function for the Job
Reach for a timing-function tool when the keyword easings are not enough. The keywords, linear, ease, ease-in, ease-out, ease-in-out, cover the common cases. ease is the default for transitions: a gentle acceleration and deceleration. Use ease-out for elements that enter the viewport, because it slows down as it arrives, which feels natural. Use ease-in for elements that leave, because it speeds up as it departs. linear is the right choice when you want a constant speed: for a progress bar, for a spinner, for any motion that should not call attention to itself. The tool becomes necessary when you need a curve that is not symmetric, or that has a spring-like feel, or that matches a brand’s motion language. But before you use a tool, ask whether a keyword would do. The page loads faster with fewer bytes, and the code is more readable to the next developer.
The steps() and linear() Alternatives, and When They Beat Cubic-Bezier
Cubic-bezier is not the only easing function. For discrete, snapped motion, steps() is the tool. steps(4, jump-end) divides the animation into four equal segments, with the value changing abruptly at each step. This is the classic sprite-sheet technique: you have a sprite with four frames, and you step through them. The jump-end position means the value stays at the start until the first step boundary, then jumps, and ends exactly at the last step. The alternative spelling end is equivalent. If you need a pause at both the start and the end, jump-both gives you that, which is what step-start and step-end do for a single step. The spec defines these positions, and all are widely available. Check caniuse.com for current support details.
When to Use `linear()`
For a piecewise linear curve, linear() from CSS Easing Level 2 lets you specify breakpoints. linear(0, 0.5, 1) is a straight line, but linear(0, 1, 1) holds at the end. This is useful for approximating a complex curve with a small number of segments, which can be cheaper to compute than a cubic-bezier with extreme overshoot. The engine interpolates linearly between the stops, so the computation is trivial. In practice, linear() is rarely needed unless you are doing something like a bounce that would otherwise require multiple cubic-bezier segments. The spec is still a draft, but Chrome and Firefox have shipped it. If you are targeting older Safari, stick with cubic-bezier or steps().
The Unoptimised Keyframe Declaration, and How to Trim It
A common tool output for keyframes includes redundant declarations. Consider this:
@keyframes slide-in {
0% {
-webkit-transform: translateX(100%);
-moz-transform: translateX(100%);
-o-transform: translateX(100%);
transform: translateX(100%);
}
100% {
-webkit-transform: translateX(0);
-moz-transform: translateX(0);
-o-transform: translateX(0);
transform: translateX(0);
}
}
The unprefixed transform is the only one any current engine reads. The -webkit- version was needed for Safari 8 and earlier, but Safari 9, released 2015, dropped the requirement. The -moz- and -o- prefixes never appeared in a shipping engine for transform inside keyframes. The hand-written version is three lines:
@keyframes slide-in {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
Use from and to instead of 0% and 100%. They are identical, and from/to is shorter and clearer. If you are animating from the element’s current state to a target, omit the from block entirely. The engine uses the computed value at the time the motion starts. That is a subtle but useful optimisation: it means the keyframe does not hard-code a starting position, so the motion works even if the element’s layout changes before it fires.
The PostCSS Easing Plugin, and Why You Might Not Need It
There is a PostCSS easing plugin that transforms ease-in-out-back into a cubic-bezier() string. It is a convenience for developers who want a named syntax for common curves. The plugin works, but it adds a build dependency for a feature that is a one-line substitution. If you already use PostCSS for other things, autoprefixer, adding the plugin is low cost. But if you are not running PostCSS, the plugin is not worth the setup. The hand-written equivalent is to define a custom property on :root:
:root {
--ease-back: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
Then use var(--ease-back) in your transitions and animations. This gives you the named syntax without a plugin, and it has the advantage of being overridable in a media query or a theme. The plugin’s output is the same string, but it is generated at build time, so you cannot change it at runtime. The custom property wins for flexibility. If you are shipping motion that must be tuned per breakpoint, the custom property is the way.
What a Tool’s Output Does to Your File Size
A verbose output, with its four prefixed transition declarations, four prefixed animation declarations, and a will-change line, adds roughly 400 bytes of uncompressed CSS. After gzip compression, the repetition collapses because gzip exploits exact repeats, so the prefixed lines compress to a fraction of their size. The actual bytes over the wire are not the problem. A few hundred bytes is nothing. The problem is that those bytes are not free: they are maintenance debt. A future developer reads the file, sees -moz-transition, and wonders if there is a legacy Firefox user they need to support. They cannot know it is a tool artifact. They may spend an hour testing in an old engine that does not exist anymore. The cost of stale prefixes is not bandwidth; it is developer time and the risk of a mistake when someone “cleans up” the file and removes a prefix that some other bit of code actually needed.
The FAQ: What You Actually Need to Know
Do I need vendor prefixes for cubic-bezier or transitions in 2026? No. Every engine in the Baseline set has supported unprefixed transitions and animations since 2015. Prefixes are dead weight. If you see a tool emitting them, treat that output as a template, not as a final deliverable.
What is the fallback if an engine does not support cubic-bezier? No engine in the last decade lacks support. But if you are writing for a hypothetical ancient client, an unrecognized timing-function value falls back to ease. A @supports guard is unnecessary; the fallback is automatic.
Why does my motion jank even with a good cubic-bezier curve? Because you are animating a property that triggers layout or paint. The curve only affects the speed profile; it does not change the rendering cost. Switch to transform and opacity, and the compositor will handle it smoothly.
Can I use will-change to make every motion smoother? No. will-change promotes an element to a compositor layer, which consumes memory. Applying it to many elements can exhaust GPU memory and slow the page. Use it only for elements that actually animate repeatedly, and remove it when the motion is done.
Who This Subject Suits, and Who Should Skip It
This subject suits the working front-end developer who ships CSS to production and needs to know which parts of a tool’s output are safe to keep and which are noise. It suits the performance-conscious developer who has seen motion jank and wants to understand why the curve is not the culprit. It suits the technical writer who must explain easing without laundering a tool’s defaults into facts. It does not suit the person learning CSS from zero. Go read the MDN guide on transitions and animations first, then come back. It does not suit the designer who wants to know why a layout looks good. That is a different question about space and hierarchy, not about timing functions. And it does not suit anyone who is happy to paste tool output and hope for the best, because that person will not do the one thing this guide asks: read the output, judge it, and replace it with something you can defend.