Workshop · back to the section

Why does the first frame of the animation jump?

The movement code was correct, and the box still jumped on the way in, then moved beautifully from there. The cause sat two steps away.

At the bottom of the search panel a footer line says how many results there are, and says so when there are none. That line opens from zero height, from JavaScript: a follower writes a height onto it every frame, and the number travels the whole way along the curve it should.

On screen it jumped. On the first frame it stood at almost full height, and moved smoothly from there — which is worse than jittering all the way, because the second half of the movement is perfectly smooth, so the movement code looks fine.

What does measuring show when the code looks fine?

Reading the movement code gets you nowhere here, because there is nothing wrong with it. What does get you somewhere: printing two numbers side by side on every frame — what the script sets, and what the box actually takes up.

Frame-by-frame trace of the footer opening — the height set and the height taken
$ ~/.kk-venv/bin/python tipp-nyom.py
  116.0ms   h=  20.19   step= +19.19   style.height=      1px
  184.0ms   h=  20.30   step=  +0.11   style.height=   20.3px
  201.0ms   h=  20.50   step=  +0.20   style.height=   20.5px
  217.0ms   h=  20.69   step=  +0.19   style.height=   20.7px
  234.0ms   h=  21.00   step=  +0.31   style.height=     21px

The two numbers disagreed. The script set the height to one pixel, and the box still took up 20.19 pixels. From there the question is what stops the box from being shorter than one pixel.

What puts a floor under the box?

The 20.19 works out on paper: 9.6 plus 9.6 plus 1. Padding at the top, padding at the bottom, a one-pixel border. box-sizing: border-box means the height you set includes the border and the padding — and padding cannot be compressed. Padding puts a floor under the box, and the height animation stays invisible until it clears that floor.

The floor under the box With the shorthand the floor is at its final value straight away: the height the script sets starts from the bottom, but while it stays below the floor the box does not move at all, so it jumps there on the first frame. With the two longhands the floor rises together with the height, stays below it throughout, and the box follows the movement. with the shorthand jump the height the script sets floor — padding and border with the two longhands the box follows throughout the floor rises with it
with the shorthandthe floor — padding and border — is at its final value at once
with the two longhandsthe floor rises with it
with the shorthandthe height the script sets stays below the floor — jump
with the two longhandsthe box follows throughout

the script writes the same height in both cases

Why was the padding already at its final value?

Because the padding was meant to animate too, and did not. The stylesheet held a single transition list with three properties: opacity moved, border-color moved, and padding-block arrived at its final value on one frame. That is the point where the floor was already in place while the height was still coming up from below.

The cause is engine-specific. WebKit starts no transition on the padding-block shorthand, while it does on padding-block-start and padding-block-end. Chromium animates the shorthand as well — meaning the whole thing looked flawless in the preview browser.

Which property gets a real transition — measured in WebKit, two frames after the change
$ ~/.kk-venv/bin/python pad-teszt.py
  transition: padding-block          → 20px      JUMPED
  transition: padding-block-start    → 1px       animates
  transition: padding-top            → 1.1px     animates
  transition: opacity                → 0.056667  animates

The measurement has a trap in it. Read immediately after the change, the computed value already gives the final one, because the transition only starts on the next style recalculation. From that it looks as though no property animates at all — the read has to happen two frames later.

What was the fix?

Two lines instead of one, with the same duration and easing.

The transition list on the footer lineCSS
transition: opacity var(--t-felulet) var(--ease-oda-vissza),
            padding-block-start var(--t-felulet) var(--ease-oda-vissza),
            padding-block-end var(--t-felulet) var(--ease-oda-vissza),
            border-color var(--t-felulet) var(--ease-oda-vissza);

With that the padding grows together with the height. The floor rises too, only more slowly than the height moves away from it: the full padding and border come to 20.19 pixels, the full height to 36.8 — so the box never catches up with its own floor. Measured, the first step came down from 19.2 pixels to 1.5, against a threshold of 3.5.

What generalises from this?

The first thing is the distance between the symptom and the cause. When an animated size jumps at the start and is smooth afterwards, the movement is usually not the problem: something puts a floor under the box, and the first stretch of the movement happens below that floor, out of sight. Padding puts one down, so does the border, a min-height, the line height, and in flex layout the automatic minimum size of the item.

The second is the shorthand. In a transition list a shorthand property is a risk: when the engine does not recognise it as animatable it does not error, it jumps — and the jump does not show up where the shorthand sits. The same suspicion applies to padding, margin, inset and padding-inline.

An earlier round already slipped on this. Back then a min-height: 0 went into the rule for this same 20.19 pixels, and it did solve another showing of the symptom — the number it did not, because the floor was still there, coming from somewhere else.

The check that found this one produced a number — a per-frame peak against a threshold. On what is worth handing to a machine when a site gets reviewed, there is a separate piece.

Questions on this

Why does a height animation jump on the first frame?

Usually the movement is not at fault: something puts a floor under the box. The height being set stays below that floor for a while, so the box does not move, and the moment the height clears the floor the box arrives there all at once. The second half of the movement is perfectly smooth, which is why the movement code looks fine.

What puts a floor under a box?

With box-sizing: border-box the height you set includes the border and the padding, and padding cannot be compressed. Nine and a half pixels of inner spacing top and bottom plus a one-pixel border means the box cannot be shorter than about twenty pixels, whatever the script writes on it. A min-height, the line height, and in flex layout the automatic minimum size of the item put down a floor the same way.

Which transition properties are worth suspecting?

The shorthands. WebKit starts no transition on the padding-block shorthand, while it does on padding-block-start and padding-block-end; Chromium animates the shorthand too, so the difference only shows on one engine. The same suspicion applies to padding, margin, inset and padding-inline.

How do you measure whether a transition actually runs?

Read the computed value two frames after the change, not immediately after it. The transition starts on the next style recalculation, so a synchronous read still returns the final value — from which it looks as though no property animates at all.

← Back to the Workshop