Skip to main content

Harshal V. LADHE

An Interactive Guide to the CSS linear() Easing Function

Chain custom stops into springs, bounces, and elastics.
Published at:
Last updated:
Estimated reading time:27 min read

Introduction

Drop a ball on a hard floor and watch it settle. It doesn't hit the ground once and stop — it bounces, a little lower each time, then a little lower again, until the bounces are too small to see and it's just resting. Three, four, sometimes five distinct rises and falls before it's actually still.

Now try to describe that motion with a single cubic-bezier() curve. You can't — not because you haven't found the right four numbers yet, but because a cubic bezier is structurally incapable of it. It's one polynomial, so progress can change direction at most twice: enough for a single overshoot or a single wind-up, never for a ball that reaches the floor, rises away from it and comes back three separate times. That's not a harder curve to tune; it's a different category of curve entirely — and by the Bouncing Ball recipe further down, you'll have drawn exactly that ball.

CSS's linear() easing function is that different category. Instead of two control points pulling on a fixed cubic shape, it takes a plain list of numbers — as many as the motion needs — and draws a straight line between each one in turn. No polynomial, no ceiling on how many times it can change direction, no limit on how strange the shape gets. An Interactive Guide to CSS cubic-bezier() introduced linear() briefly, as the way out for shapes a single cubic curve can't reach. This post is the follow-through: the full syntax, the auto-spacing rules that make short linear() calls readable, a draggable playground with as many stops as you want, and the spring, bounce, and anticipation recipes that linear() was practically invented for.

Why cubic-bezier() Has a Ceiling — and linear() Doesn't

Every named timing function — linear, ease, ease-in, ease-out, ease-in-out — and every custom cubic-bezier(x1, y1, x2, y2) value is the same kind of object underneath: a single cubic polynomial pinned at (0, 0) and (1, 1), shaped by two control points in between. That's an enormously flexible shape — the entire "Beyond the Defaults" section of the cubic-bezier() guide is built on pushing those two points harder — but it's still fundamentally one curve, and one curve can only bend so many times before it runs out of room.

Concretely: a cubic bezier's y value, as a function of x, can have at most one local maximum and one local minimum. That's enough for one overshoot past the target — the "punchy pop" curves from the cubic-bezier() guide — but it's not enough for a second overshoot, a third bounce, or a shape with a flat plateau in the middle. Those all require the curve to change direction more times than a single cubic polynomial is capable of.

linear() sidesteps the whole problem by not being a polynomial at all. It's a piecewise-linear function: a plain list of points, connected corner to corner by straight line segments.

/* Every value here is a point the line passes through. Add a stop,
   the shape gets one more corner to bend at — no ceiling. */
transition-timing-function: linear(0, 0.5, 1);
transition-timing-function: linear(0, 0.63 25%, 0.9 45%, 1.04 65%, 1);

The first line is just three points along the diagonal — visually indistinguishable from the plain linear keyword, because three evenly-spaced points on a straight line is a straight line. The second line is the same overshoot-and-settle example from the cubic-bezier() guide: progress climbs past 1 and settles back onto it. That one overshoot alone is something a cubic-bezier() could manage — but add a few more shrinking stops and the line can swing across 1 again and again, a pattern of direction changes no cubic-bezier() call could ever produce (the ten-stop elastic curve further down does exactly that). That's the entire idea of linear() in one comparison: trade a fixed, smooth, two-point shape for an open-ended list of straight segments.

Anatomy of a linear() Stop

Formally, linear() takes a comma-separated list of stops, and each stop is:

<number> [<percentage>]?
  • The number is required. It's a progress value — exactly the same y-axis quantity from the easing functions guide's time-vs-progress model. 0 means "still at the start," 1 means "fully at the end," and like a bezier control point's y, it's completely unbounded: negative values and values past 1 are both valid, which is where overshoot and anticipation come from.
  • The percentage is optional. When present, it pins that stop to an exact point along the transition's duration — 25% means "a quarter of the way through the time, progress should be exactly this value." When absent, the stop's position is worked out automatically.

That second part — what happens to a stop with no percentage — is the detail that makes linear() pleasant to write by hand instead of a chore.

Auto-Spaced Stops

A stop with no percentage doesn't sit at some default location; it's spread evenly across whatever room is left between its pinned neighbours. The rule, in full:

  • If the first stop has no percentage, it's treated as 0%.
  • If the last stop has no percentage, it's treated as 100%.
  • Any run of consecutive un-pinned stops between two pinned ones (including those two boundary defaults) is spaced at equal intervals across that gap.

Walk through a concrete example — linear(0, 0.3, 0.6, 0.9 80%, 1) — and the resolved percentages are:

StopWritten asResolved position
0no percentage (first stop)0%
0.3no percentage26.7%
0.6no percentage53.3%
0.980%80%
1no percentage (last stop)100%
ProgressionTime

Every linear() graph in this post uses the same layout: time runs left to right, progress runs bottom to top, the two dashed lines mark 0 and 1, and each dot is one stop. The vertical range extends a little beyond 0–1 so that overshoots and wind-ups stay visible.

The first stop defaults to 0%, the last defaults to 100%, and the two un-pinned stops in between — 0.3 and 0.6 — split the 0%–80% gap into three equal thirds. Nudge that 80% to 50% instead, and those same two stops would slide left with it, still evenly spaced between 0% and the new boundary. This is exactly why the plain linear(0, 0.5, 1) from the last section behaved like a straight line: with no percentages anywhere, every stop just distributes evenly across the full 0%–100% range, which is a straight diagonal by definition.

Visual demo: a marker races across a track using the 0.9 pinned at 80% easing curve, with a static trail showing how its speed is distributed over time.

Flip between the two and watch the ghost trail: moving one pin from 80% to 50% doesn't just move that stop — it squeezes both auto-spaced stops before it into the first half of the duration, so the whole first stretch of the motion speeds up with it, and the final 0.9 → 1 crawl stretches from a fifth of the duration to half of it.

Pinned Stops with a Percentage

A percentage is how you break out of even spacing and sculpt something asymmetric — the same role a bezier control point's x plays, but per-stop instead of a single shared pull. The overshoot-and-settle example from earlier leans on this directly: 0.63 25%, 0.9 45%, 1.04 65% pins three of the five stops at chosen points. That front-loads the motion — 63% of the distance in the first quarter of the time — then slows through 0.9 at 45%, peaks just past the target at 65%, and leaves the final 35% of the duration for a gentle settle back onto 1. Even spacing could never produce that fast-then-lingering rhythm; the pins are what shape it.

Reading and Writing Multiple Comma-Separated Stops

Everything above scales to as many comma-separated stops as the motion needs — there's no fixed arity the way cubic-bezier() is permanently stuck at four numbers. Each additional stop is one more corner the line is allowed to bend at, and one more chance to point at a specific moment in time with a specific progress value. A slightly richer elastic-settle curve, annotated stop by stop:

.toast {
  /* Ten stops: a fast overshoot, three shrinking oscillations, then rest.
     Every middle stop is pinned, so the opening climb is quick and each
     later swing gets roughly the same slice of time as the one before. */
  transition-timing-function: linear(
    0,          /* start, always 0% */
    0.42   9%,  /* accelerating out */
    0.83  18%,  /* still climbing */
    1.15  32%,  /* first overshoot past the target */
    0.92  45%,  /* swings back under */
    1.06  58%,  /* second, smaller overshoot */
    0.97  70%,  /* smaller undershoot */
    1.02  83%,  /* third, tiny overshoot */
    0.99  92%,  /* barely visible correction */
    1          /* settle, always 100% */
  );
  transition-duration: 900ms;
}
ProgressionTime

Plotted, the rhythm is easy to read: each swing across the y = 1 line is smaller than the one before (1.15, then 1.06, then 1.02 on the high side), while the time between swings stays roughly even. That's how a real damped spring behaves — it loses height on every swing, not speed of swinging.

A few things worth noticing in that list, because they generalise to every linear() value you'll write or generate:

  • Order matters, and it's positional, not sorted. The browser draws segments in the order the stops are written, connecting each one to the next. It doesn't re-sort by percentage first — which is exactly why an out-of-order percentage gets clamped upward rather than silently reordered, as the warning above covers.
  • Whitespace and newlines are free. CSS doesn't care that the value above spans eleven lines with comments — it's parsed as a single comma-separated list either way. Long, generated linear() values are far more reviewable formatted one stop per line than crammed onto one, the same way a long grid-template-areas value benefits from line breaks purely for the human reading it later.
  • The value never has to be monotonic. Unlike the percentage (position), which is enforced non-decreasing, the number (progress) is free to go up, down, up again, in any pattern — that freedom is the entire reason linear() exists. A list of values that only ever increases would just be a fancier way to write a straight line.
  • Two stops is the floor. linear(0, 1) is valid and equivalent to the plain linear keyword — a single straight segment. There isn't a documented hard cap on the other end; the practical ceiling is legibility, not the spec (more on sane stop counts in the Common Mistakes section below).

Try It: The linear() Playground

Reading resolved-position tables builds the mental model; dragging a curve until it looks like the bounce you're picturing builds the intuition. Unlike the two-handle cubic-bezier() playground in the cubic-bezier() guide, this one has to support an arbitrary, changing number of points — so it comes with its own add/remove/pin controls instead of just two draggable handles.

ProgressionTime
linear(0, 0.63 25%, 0.9 45%, 1.04 65%, 1)
  • Stop 1fixed at 0%
  • Stop 2
  • Stop 3
  • Stop 4
  • Stop 5fixed at 100%

A short tour of the controls:

  • Drag a filled handle to change both its value and its pinned position — filled handles are pinned to an exact percentage, shown with a solid dot.
  • Drag a muted handle and only its value moves; muted handles are auto-spaced, so their horizontal position keeps tracking the even-spacing rule as you and every other stop change.
  • The pin checkbox in the stop list below the graph flips a stop between the two modes — check it to freeze the stop's current position as an exact percentage, uncheck it to hand that position back to auto-spacing.
  • Add stop inserts a new, unpinned stop at the midpoint of whichever gap is currently widest — the fastest way to add a bend to the flattest-looking part of the curve.
  • Type exact numbers into a stop's value and pin at fields when dragging isn't precise enough, or remove a stop entirely with its × button.
  • Load a preset drops in one of five curated shapes — Overshoot & Settle, Bounce, Spring, Anticipate, and Smooth Step — each discussed in detail in the next section. The picker names the preset your curve still matches, and switches back to "Load a preset…" as soon as you change a stop. Reset brings back the starting Overshoot & Settle curve.
  • Read the faded dots on the track below the graph: each one marks where the box will be at an equal slice of time, so bunched-up dots mean slow motion and spread-out dots mean fast motion. Press Play to watch the box run the curve for real — it uses your exact linear() value as its timing function, so what you see is what the browser will do.
  • Every drag and edit updates the linear(...) readout live; the copy button next to it grabs the exact value shown, ready to paste into a stylesheet.

Building Real Motion With linear()

With the mechanics in hand, here's what they're actually for. Every preset in the playground above corresponds to one of these.

Overshoot and Settle

transition-timing-function: linear(0, 0.63 25%, 0.9 45%, 1.04 65%, 1);
ProgressionTime

This is the running example from earlier, and the playground's starting curve: progress races to 0.63 in the first quarter, peaks just past 1 at 65%, and spends the last 35% of the time settling back. One small overshoot is honestly within cubic-bezier()'s reach — which is why the fallback later in this post can approximate it with one. Its value here is that it's short enough to read at a glance and mixes pinned and auto-spaced stops, which makes it the easiest curve to start editing from.

Bouncing Ball

.ball {
  transition-timing-function: linear(
    0, 0.11 12%, 0.44 24%, 1 36%,       /* the drop, speeding up */
    0.82 45%, 0.75 55%, 0.82 64%, 1 73%, /* first bounce */
    0.94 82%, 1 91%,                     /* second, smaller bounce */
    0.98 95%, 1                          /* a last little hop */
  );
}
ProgressionTime

This is the ball from the introduction, and the clearest example of something no cubic-bezier() can draw. Notice that progress never goes above 1: the floor is the target, and a ball can't pass through it. Instead the curve touches 1 four separate times, bouncing away from it in between.

Three details make it read as a real bounce rather than a wobble:

  • The drop speeds up. The two stops at 12% and 24% make the first segment curve upwards, the way gravity accelerates a falling ball.
  • Each bounce is lower. The ball rises back to 0.75, then 0.94, then 0.98 — that is, it climbs a quarter of the way back up, then about 6%, then 2%.
  • Each bounce is shorter. The time between landings halves every time: 36% → 73%, then 73% → 91%, then 91% → 100%. A lower bounce is also a quicker one.

Each arc here is drawn with just two straight segments, so it looks slightly angular up close. Add a stop or two inside each arc and the hops become smoother parabolas — the usual linear() trade of length for smoothness.

Spring Physics

transition-timing-function: linear(0, 0.5, 1.09, 0.97, 1.01, 1);
ProgressionTime

No percentages at all: six evenly spaced stops that shoot past 1, dip back under it, nudge over it once more and come to rest. Each swing is smaller than the last, which is what makes it read as springy rather than bouncy.

This is genuinely how most "spring easing" linear() values you'll find in the wild are produced — not hand-tuned stop by stop, but generated by running a spring simulation once and dumping its output straight into a linear() list. The Spring preset in the playground above is a hand-picked, six-stop simplification of exactly that process — real generated springs commonly run to several dozen stops for a smooth result, which is normal and not a sign you've done something wrong.

Anticipation (Wind-Up)

transition-timing-function: linear(0, -0.2 15%, 1);
ProgressionTime

A small dip below 0 before the curve ever starts climbing — a wind-up, like a coiled spring pulling back before it's released. The cubic-bezier() guide's cubic-bezier(0.36, 0, 0.66, -0.56) "Anticipation" recipe does something similar with a bezier curve, but only as a dip on one end of the motion. Because linear() has no limit on how many times the line can change direction, you can combine a wind-up and a multi-bounce settle in the very same value — exactly what the "Try this first" exercise in the playground section builds.

Smoothing a Middle Section

transition-timing-function: linear(0, 0.2, 0.8, 1);
ProgressionTime

Four evenly spaced stops, so each segment gets a third of the time: progress only moves 0.2 in the first third, a full 0.6 in the middle third, and 0.2 again in the last. That's an "S" shape — slow start, fast middle, slow finish. It's a rougher, more angular cousin of ease-in-out, and it's a good starting shape to reach for when a design calls for "something like ease-in-out, but I want to hand-tune the middle" without committing to bezier math.

Visual demo: a marker races across a track using the Bounce easing curve, with a static trail showing how its speed is distributed over time.

Race the five recipes back to back — the graphs above show each shape, but the race is where the difference in feel between a settle, a bounce, a spring, and a wind-up actually lands. Watch the ghost trail on Bounce: the dots pile up at the right-hand end four separate times, once per landing.

Flat Segments: The Two-Percentage Stop Syntax

There's one more piece of linear() syntax worth knowing, mostly because it explains behaviour you might otherwise stumble into by accident: a stop can actually take two percentages, not just one.

/* A stop can carry a percentage *range* instead of a single point. */
transition-timing-function: linear(0, 0.5 20% 40%, 1);

/* This is exactly equivalent — two ordinary stops, same value, back to back. */
transition-timing-function: linear(0, 0.5 20%, 0.5 40%, 1);

Giving a stop two percentages duplicates it at both points, which — since two stops at the same value connect with a perfectly flat line — produces a plateau: progress freezes at 0.5 for the entire stretch from 20% to 40% of the duration, then resumes moving. The two-percentage form is purely a shorthand for the longer version underneath; both lines above produce the identical curve, and reach for whichever reads more clearly in context.

ProgressionTime

That plateau behaviour is worth knowing even if you never write the two-percentage form yourself, because it's easy to produce by accident with two ordinary stops that happen to land on the same value:

linear() vs. cubic-bezier() vs. steps()

All three of CSS's timing-function tools solve the same underlying problem — mapping elapsed time to progress — with genuinely different trade-offs:

cubic-bezier()linear()steps()
ShapeOne smooth cubic curveStraight segments through any number of pointsDiscrete, instant jumps
Control pointsExactly 2, fixed arityAny number, open-endedA count, plus a jump direction
Can it bounce more than once?No — one bend, at most one overshootYes — as many as you add stops forN/A — no interpolation at all
Interpolation between pointsSmooth (curved)Smooth in time, but linear between each pair — visible corners at each stopNone — a hard cut, no interpolation
Best forEveryday UI motion — entrances, exits, in-place state changesSprings, multi-bounce settles, and any shape two control points can't expressSprite sheets, ticking clocks, typewriter reveals
Typical value lengthAlways 4 numbersGrows with the motion's complexity1 number plus a keyword

The corner-vs-curve distinction in that "interpolation between points" row is worth sitting with for a second: linear()'s segments are individually straight, so a linear() curve with only a handful of widely-spaced stops can look visibly angular up close, with sharp direction changes at each stop instead of cubic-bezier()'s smooth bend. More stops make the corners less noticeable, at the cost of a longer value — the same smoothness-for-verbosity trade every piecewise approximation makes.

Browser Support and Safe Fallbacks

linear() is a newer addition to the timing-function family than cubic-bezier() and steps(), which have been around for well over a decade. Firefox and Chrome shipped it in spring 2023 and Safari followed in version 17.2 that December, so every current major browser supports it. The only reason to think about a fallback is if your project still has to serve browser versions from before then.

If it does, the safe pattern is short: declare a cubic-bezier() fallback first, then the linear() value you actually want, on the same property.

.toast {
  /* A browser that doesn't understand linear() treats the second
     declaration as an invalid value and simply ignores it — keeping
     this cubic-bezier() approximation instead of falling through to
     nothing, or to the CSS default `ease`. */
  transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
  transition-timing-function: linear(0, 0.63 25%, 0.9 45%, 1.04 65%, 1);
}

This works because of how CSS handles a single property declared twice inside the same rule: a browser that doesn't recognise the linear() function treats that entire declaration as invalid at parse time and drops it, leaving the previous, valid cubic-bezier() declaration in effect. A browser that does understand linear() simply applies the later declaration as normal, since later same-specificity declarations win. Either way, every browser ends up with a working timing function — just a smoother one on newer engines.

Performance and Accessibility

A linear() timing function costs the browser's compositor essentially the same as a cubic-bezier() one at runtime: both reduce to "look up the progress value for this moment in time," whether that lookup solves a cubic polynomial or walks a list of segments. Everything the cubic-bezier() guide's Accessibility and Performance section says about preferring transform- and opacity-driven, GPU-friendly properties applies exactly the same way here — the timing function doesn't change which properties are cheap to animate.

Where linear() genuinely differs is stylesheet weight, not runtime cost: a ten-stop value is a longer string to parse and ship than a four-number cubic-bezier() call, and a generated spring curve with forty or fifty stops is longer still. That's a trivial cost for one timing function on one element, but it's worth keeping an eye on if a design system embeds a large generated linear() value as a repeated CSS custom property across many components — the same "keep your bundle lean" instinct that applies to any other repeated, sizeable string in a stylesheet.

On the motion side: the playground's Play button checks prefers-reduced-motion and disables itself when it's set, exactly like every race demo in the easing functions and cubic-bezier() guides — the point of that demo is motion some readers have explicitly asked not to see, so it shouldn't play regardless. Dragging stops, reading the graph, and copying the generated value all stay fully available either way, since none of that requires anything to actually move. The same rule applies to any spring, bounce, or elastic curve you ship for real: wrap it in @media (prefers-reduced-motion: reduce) just as you would an overshoot cubic-bezier(), since a multi-bounce linear() curve is, if anything, more motion than a single overshoot — not less.

Common Mistakes and Gotchas

  • Reaching for linear() when a keyword would do. linear(0, 1) and the plain linear keyword are equivalent, and a three-or-four-stop linear() value that's just approximating ease-in-out is usually more legible written as cubic-bezier() or the keyword itself. Save linear() for shapes that genuinely need more than one bend.
  • Assuming the value has to keep increasing, like the position does. Percentages are enforced non-decreasing (with the clamp-upward behaviour covered earlier); the numbers themselves are free to rise and fall in any pattern — that's the entire feature, not an edge case to work around.
  • Forgetting a fallback when you support older browsers. Spring and bounce generators output raw linear() values with no cubic-bezier() declared alongside them. If your audience includes browsers from before 2024, add one, even a rough approximation — the pattern in the Browser Support section above costs one extra line.
  • Letting stop counts balloon past what the motion needs. There's no spec-enforced ceiling, but a 200-stop value copied wholesale from a simulation's full output is usually the same shape a well-chosen 30–40 stops would produce, at a fraction of the size — down-sample before shipping, not after something looks wrong.
  • Confusing an accidental plateau for a bug. Two consecutive stops landing on the same value produce a flat segment, which reads as a visible pause. If a linear()-eased element seems to hitch partway through, check for repeated values before assuming something else broke — see the flat segment section above.

Frequently Asked Questions

Can linear() be animated — can a transition morph from one linear() shape to another?

No, for the same reason cubic-bezier() can't: the timing function isn't an animatable property. One linear() value governs an entire transition (or one per segment, in a multi-keyframe animation), start to finish. To make a curve's shape appear to change mid-motion, chain separate transitions or keyframe segments, each with its own timing function, rather than expecting one value to morph live.


Does linear() work with animation-timing-function on individual @keyframes selectors, the same way cubic-bezier() does?

Yes — linear() is accepted anywhere a timing function is, including as an override on a single keyframe selector inside @keyframes, exactly like the keyword and cubic-bezier() values covered in the easing functions and cubic-bezier() guides.


Can I use linear() from JavaScript with the Web Animations API?

Yes. element.animate() accepts any CSS timing function as its easing option, linear() included — element.animate(keyframes, { duration: 700, easing: "linear(0, 0.5, 1.09, 0.97, 1.01, 1)" }) runs the same spring as the CSS version above. That makes it easy to generate a curve in JavaScript, for example from a spring simulation, and apply it without writing any CSS at all.

Wrapping Up

cubic-bezier() is one smooth curve built from exactly two control points — expressive, but capped at a single bend no matter how hard you push those points. linear() removes the cap entirely by trading the smooth curve for a straight-segmented one built from as many stops as the motion calls for, each a plain progress value with an optional percentage that pins it to an exact moment in time. Leave the percentage off and a stop spreads evenly across whatever room its pinned neighbours leave it — which is exactly why sampled data, like a spring simulation's output, drops straight into linear() with no extra maths required.

Key takeaways:

  • linear() takes a comma-separated list of stops — <number> [<percentage>]? — with no fixed limit on how many, unlike cubic-bezier()'s permanent four.
  • A stop's number is the progress value (unbounded, same as a bezier control point's y); its optional percentage pins it to an exact point in the transition's duration.
  • Stops with no percentage auto-space evenly between their pinned neighbours — the first defaults to 0%, the last to 100%, which is why uniformly-sampled data (like a spring simulation) needs no percentages at all.
  • Percentages must be non-decreasing; an out-of-order one is silently clamped upward to the largest seen so far, collapsing that segment rather than running backward.
  • A stop can carry two percentages instead of one, which duplicates its value across that range and produces a flat, paused segment — the same effect two ordinary equal-value stops produce.
  • linear() costs the same at runtime as cubic-bezier(); the real trade-off is stylesheet weight, so down-sample a generated curve to what the motion actually needs.
  • Every current major browser supports it; if you also serve browsers from before 2024, declare a cubic-bezier() fallback first, exactly like the pattern shown above.
  • Reserve it for shapes a single bezier curve genuinely can't express — multi-bounce settles, sampled springs, deliberate flat pauses — not as a default replacement for the keywords and curves that already cover everyday UI motion.

Plotting straight lines sounds simple until every stop needs the browser's exact auto-spacing and clamping rules behind it. This one needed an open-ended playground — drag, pin, add, remove and type stops, with a live preview running the real linear() value — plus static graphs and races for every worked example, from the auto-spacing table to the ten-stop elastic settle and the flat-segment syntax. Getting the bounce, spring and wind-up presets to each feel distinct took more rounds of tuning than any curve on the cubic-bezier() side of the series. Thanks for tracing every stop with me — now go and chain together a curve of your own. 🌀

  • An Interactive Guide to CSS cubic-bezier()

    Drag two control points, design your own curve.
    An interactive guide to CSS cubic-bezier(): what the four numbers control, how to build overshoot and anticipation curves, hand-tuned recipes, and a draggable curve playground.
    Published at:
  • An Interactive Guide to CSS Easing Functions

    Feel how the five easing keywords shape motion.
    A visual guide to CSS timing functions: how time maps to progress, why ease, linear, ease-in, ease-out and ease-in-out feel so different, and when to use each.
    Published at:
  • An Interactive Guide to CSS Transitions

    Craft high-performance, responsive UI motion with CSS transitions and transforms
    Master CSS transitions for responsive UI. Learn to use duration, delay, timing-function, and transform for GPU-accelerated performance and smooth micro-interactions.
    Published at: