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.
$ ~/.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.
- 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.
$ ~/.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.
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.