Skip to content
academia.sh

Lesson 14 / 23

Transitions

Spreading a change between two states over time; values that can transition and values that cannot, computing the cubic Bezier timing curve, an interrupted transition's behavior, and stepped timing.

Contents

The previous lesson defined the card’s highlighted state: eight units up, four percent larger. The declaration gives both states, but there is no path between them. The moment the pointer is over the card, the card is already in its new place.

Motion is the spreading of the path between two states over time. This lesson covers the declarations that define that path: which property, over how long, along which speed curve, goes from its old value to its new one, and what happens if the path is cut short?

A Transition Fills the Gap Between Two Values

A transition makes a property’s computed value progress from its old value to its new one over time, once that value changes. The transition does not trigger the change; it only spreads a change that was going to happen anyway.

One warning is needed: the Visual Presentation with CSS course covered gradients — the computed visual between colors, written with functions like linear-gradient(). That is an unrelated concept; “transition” in this lesson always means change over time, never a color gradient.

A transition has four separate declarations:

Declaration Question it answers
transition-property Which properties will be spread
transition-duration How long the path will take
transition-timing-function How speed is distributed along the path
transition-delay How long after the change it will start

The transition shorthand takes all four in a single line, and comma-separated lists let a separate value be written for each property. If duration is not written, its default is zero; a transition declaration with no duration does nothing.

transition-delay can also take a negative value. A negative delay starts the transition not from its beginning but from its middle: writing a delay of 60-60 milliseconds on a 240-millisecond transition opens it already 25 percent along.

Values That Can and Cannot Transition

A transition requires being able to produce an in-between value between two values. This is defined for length, number, color, and transform lists. It is not defined for keywords.

This has consequences. A transition from height: auto to a fixed height does not work, because auto is not a number. There is no in-between value between display: none and a visible state; display type is a discrete property, and discrete properties change not in the middle of a transition but at its end or its start.

If a collapsible section’s height is meant to be spread, there are three routes: transition over a fixed upper bound, transition a grid track’s fractional value, or work with transform and opacity without touching height at all. The third is the cheapest, for reasons the following lessons will show.

The visibility property is an exception: although discrete, it keeps its old value for the duration of the transition, which is why, when written together with opacity, it keeps an element clickable while it fades in but not while it fades out.

The Timing Curve

The timing function converts the fraction of elapsed time into a fraction of progress. The input is time between 00 and 11, the output progress between 00 and 11.

Every named function is a cubic Bezier curve joining the ends (0,0)(0,0) and (1,1)(1,1) with two control points. The notation cubic-bezier(x1, y1, x2, y2) gives these two points.

Evaluating the curve cannot be done in a single step: the curve is defined against the parameter tt, while the time available is on the xx axis. First the tt that gives the xx value is found, then yy is read for that tt.

// transition.mjs — evaluating a cubic Bezier timing curve and computing in-between values
// The timing curve is between (0,0) and (1,1), with control points (x1,y1) and (x2,y2).
const bez = (a, b, t) => {
  const u = 1 - t;
  return 3 * u * u * t * a + 3 * u * t * t * b + t * t * t;   // p0=0, p3=1
};

// The curve's parameter t for a given x is found by binary search, then y is read.
function curve(x1, y1, x2, y2) {
  return (x) => {
    if (x <= 0) return 0;
    if (x >= 1) return 1;
    let lo = 0, hi = 1, t = x;
    for (let i = 0; i < 60; i++) {
      t = (lo + hi) / 2;
      if (bez(x1, x2, t) < x) lo = t; else hi = t;
    }
    return bez(y1, y2, t);
  };
}

const NAMED = {
  linear: [0, 0, 1, 1],
  ease: [0.25, 0.1, 0.25, 1],
  "ease-in": [0.42, 0, 1, 1],
  "ease-out": [0, 0, 0.58, 1],
  "ease-in-out": [0.42, 0, 0.58, 1],
};

console.log("--- progress values of the named timing functions ---");
console.log("elapsed".padEnd(12) + Object.keys(NAMED).map((k) => k.padStart(12)).join(""));
for (let i = 0; i <= 10; i++) {
  const x = i / 10;
  const row = Object.values(NAMED).map((c) => curve(...c)(x).toFixed(4).padStart(12)).join("");
  console.log(`%${(x * 100).toFixed(0)}`.padEnd(12) + row);
}

console.log("\n--- the same progress applied to two properties (240 ms, ease-out) ---");
const f = curve(...NAMED["ease-out"]);
const DURATION = 240;
const between = (start, end, p) => start + (end - start) * p;
console.log("ms".padStart(5), "progress".padStart(10), "translateY".padStart(12), "scale".padStart(10));
for (const ms of [0, 30, 60, 90, 120, 180, 240]) {
  const p = f(ms / DURATION);
  console.log(
    String(ms).padStart(5),
    p.toFixed(4).padStart(10),
    (between(0, -8, p).toFixed(2) + "px").padStart(12),
    between(1, 1.04, p).toFixed(4).padStart(10),
  );
}

console.log("\n--- interrupting the transition midway ---");
const CUT = 72;                     // ms; pointer leaves the card at the 72nd ms
const state = between(0, -8, f(CUT / DURATION));
console.log(`translateY at the moment of interruption = ${state.toFixed(2)}px`);
console.log("the return uses the same duration and curve, going from this value to 0:");
for (const ms of [0, 30, 60, 90, 120, 180, 240]) {
  const p = f(ms / DURATION);
  console.log(`  ${String(ms).padStart(3)} ms -> ${between(state, 0, p).toFixed(2)}px`);
}

console.log("\n--- discrete progress with steps(), 4 steps ---");
const step = (n, mode) => (x) => {
  if (x <= 0) return mode === "start" ? 1 / n : 0;
  if (x >= 1) return 1;
  const k = Math.floor(x * n);
  return (mode === "start" ? k + 1 : k) / n;
};
const sStart = step(4, "start");
const sEnd = step(4, "end");
console.log("elapsed".padEnd(12) + "steps(4, start)".padStart(16) + "steps(4, end)".padStart(16));
for (let i = 0; i <= 10; i++) {
  const x = i / 10;
  console.log(
    `%${(x * 100).toFixed(0)}`.padEnd(12) +
    sStart(x).toFixed(2).padStart(16) +
    sEnd(x).toFixed(2).padStart(16),
  );
}
--- progress values of the named timing functions ---
elapsed           linear        ease     ease-in    ease-out ease-in-out
%0                0.0000      0.0000      0.0000      0.0000      0.0000
%10               0.1000      0.0948      0.0170      0.1606      0.0197
%20               0.2000      0.2952      0.0623      0.3084      0.0817
%30               0.3000      0.5133      0.1296      0.4452      0.1874
%40               0.4000      0.6825      0.2149      0.5709      0.3319
%50               0.5000      0.8024      0.3154      0.6846      0.5000
%60               0.6000      0.8852      0.4291      0.7851      0.6681
%70               0.7000      0.9408      0.5548      0.8704      0.8126
%80               0.8000      0.9756      0.6916      0.9377      0.9183
%90               0.9000      0.9943      0.8394      0.9830      0.9803
%100              1.0000      1.0000      1.0000      1.0000      1.0000

--- the same progress applied to two properties (240 ms, ease-out) ---
   ms   progress   translateY      scale
    0     0.0000       0.00px     1.0000
   30     0.1986      -1.59px     1.0079
   60     0.3781      -3.03px     1.0151
   90     0.5405      -4.32px     1.0216
  120     0.6846      -5.48px     1.0274
  180     0.9065      -7.25px     1.0363
  240     1.0000      -8.00px     1.0400

--- interrupting the transition midway ---
translateY at the moment of interruption = -3.56px
the return uses the same duration and curve, going from this value to 0:
    0 ms -> -3.56px
   30 ms -> -2.85px
   60 ms -> -2.21px
   90 ms -> -1.64px
  120 ms -> -1.12px
  180 ms -> -0.33px
  240 ms -> 0.00px

--- discrete progress with steps(), 4 steps ---
elapsed      steps(4, start)   steps(4, end)
%0                      0.25            0.00
%10                     0.25            0.00
%20                     0.25            0.00
%30                     0.50            0.25
%40                     0.50            0.25
%50                     0.75            0.50
%60                     0.75            0.50
%70                     0.75            0.50
%80                     1.00            0.75
%90                     1.00            0.75
%100                    1.00            1.00

The first table compares five curves at the same points in time. Every one of them except linear is not halfway along the path at half the time.

ease completes half the path at about 30 percent of the duration; it starts fast and leaves a long, slow tail. ease-in starts slow and speeds up toward the end — at half the duration, only a third of the path has been covered. ease-out is the reverse of this. ease-in-out is symmetric and is exactly halfway along the path at exactly half the time.

The choice is not arbitrary. If an element is entering the screen, it is expected to slow down as it settles (ease-out); if it is leaving the screen, it is expected to speed up as it goes (ease-in). A symmetric curve is used for an element that stays in place and changes form.

Computing the In-Between Value

The output’s second block shows how a single progress value is applied to two separate properties. At the 60th millisecond of a 240-millisecond transition, progress is 0.37810.3781; at this fraction, the translation is 3.03-3.03 units, the scale factor 1.01511.0151.

The formula is the same for every property:

value(t)=start+(endstart)×progress(t)\text{value}(t) = \text{start} + (\text{end} - \text{start}) \times \text{progress}(t)

For transform lists, this computation is done function by function — if the same functions in the same order are present in both lists, each function’s own numbers are given an in-between value. If the lists do not match, the browser reduces both to a matrix and gives the matrices an in-between value; the result is usually not the expected path. This is why it is a notational rule for the two transform lists entering a transition to carry the same functions in the same order.

Interruption and Return

The output’s third block computes the case where the pointer leaves the card at the 72nd millisecond. At that moment, the translation is 3.56-3.56 units, and the return starts from this value, not from 8-8 units.

The rule is: when a transition is interrupted, the new transition starts from the property’s computed value at that moment. This is what keeps motion from jumping.

But it leaves a side effect: the return also uses the full duration. The card lifts for 72 milliseconds and descends over 240 milliseconds. A short touch is answered with a long return, and the motion feels delayed. To prevent this, entry and exit durations are written separately: the exit duration is kept shorter than the entry.

A transition declaration, when written on the element’s base state, applies to both the outward and the return path; when written only on the :hover rule, only the outward path spreads, and the return is instantaneous.

Stepped Timing

The steps(n, mode) function produces progress not continuously but in steps. The output’s last block compares two modes with four steps.

end mode starts at zero and drops the last step at the end; start mode drops the first step immediately and never shows the zero value. With four steps, progress only takes the values 00, 0.250.25, 0.50.5, 0.750.75, 11; everything in between is skipped.

This is used for a frame-by-frame indicator or a counter. In places where continuity is not wanted, stepped timing makes motion less distracting.

Transition on the Station Page

/* motion.css — step 2: spreading the card highlight over time */
.measurement-cards > .card {
  transform-origin: center bottom;
  transition:
    transform 240ms ease-out,
    box-shadow 240ms ease-out;
}

.measurement-cards > .card:hover,
.measurement-cards > .card:focus-within {
  transform: translateY(-8px) scale(1.04);
  transition-duration: 160ms;
}

The transition declaration is written on the base state; the return spreads too. In the highlighted state, the duration is pulled to 160 milliseconds: entry short, return somewhat longer. This ordering makes the sequence both responsive and smooth.

transform and box-shadow were listed individually. Writing transition-property: all is less to type, but it draws every computed-value change into the transition; a color or size declaration added later starts spreading unintentionally. Listing properties prevents this surprise.

Summary

  • A transition does not trigger a change; it spreads a property whose computed value is already changing over time, from its old value to its new one.
  • Values with no in-between value cannot transition: discrete properties like auto and display, and keywords, produce a jump rather than a path.
  • The timing function converts a fraction of elapsed time into a fraction of progress; named functions are cubic Bezier curves, and other than linear, half the time is not half the path.
  • An interrupted transition starts again from the property’s computed value at that moment and uses its full duration; entry and exit durations are balanced by writing them separately.
  • steps(n, mode) produces progress in steps; start drops the first step immediately, end drops the last step at the end.

Next Step

A transition depends on a condition: the value has to change. If a value does not change on its own — a loading indicator that keeps spinning, a warning that alternates between two colors — a transition has no event to trigger it. The next lesson covers the mechanism that writes the timeline into the declaration itself: intermediate stops, repeat count, and direction.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close