Hover the strip above, or tab through it. I made these nine in August for the sidebar of the app at my day job, the one my visual-regression system screenshots. My lead named them as the example of extra changes nobody asked for, which belong in their own merge request.
The ticket I filed for them myself says hovering a row highlights it while its Font Awesome glyph does nothing, so the menu feels flat. Each icon plays once per hover and takes the row’s color, graying out and turning primary with it. It also stays the size of the glyph it replaces.
Turn one tooth and stop
The gear has six teeth, so after a 60° turn it draws the same picture it started from. It overshoots to about 63.7°, settles on 60° and stays there rather than turning back.
<animateTransform attributeName="transform" type="rotate" dur="0.6s"
begin="indefinite" repeatCount="1" fill="freeze"
values="0 256 256;1.044 256 256;…;63.660 256 256;…;60 256 256"/>
Three more end the same way. Both lists scroll exactly one row as a new one rises from below, and the invoice swaps its contents for identical copies. The checklist’s bottom-row dot also splits into a tick. The other five return every value to where it began, the way Taquito’s states pass through one sitting frame.
In Firefox at 36 pixels, every first frame matches the static drawing, and so do 7 of the 9 last frames. The checklist’s last frame differs on about 15 pixels and the gear’s on about 80. A still gear drawn at 60° differs by the same count, so that gap is anti-aliasing.
A value for every frame
A font glyph moves as one piece, so each icon became separate strokes and shapes. Their 80 animated properties each hold a value every 10 to 17 ms, joined by straight lines, with no keySplines or keyTimes, so the values carry the easing. After a short ease-in, the list rows close about 15% of the distance left on each frame, and the gear and the gauge’s needle overshoot before settling. None of the 80 touches opacity, because a fade reads as mush at this size.
The cost is bytes, 64 KB for the nine files, or 15 KB gzipped, and the box carrier, with 38 tracks, is 31 KB of the 64.
The exported artwork wouldn’t run
The first exports carried keyTimes, even spacing rounded to two decimals and worth 6.8 ms at most, yet their presence stopped the animations outright, so they’re gone. The box carrier’s tracks also hung off one shared trigger, which didn’t start reliably once Angular rendered the icon, so script now starts each track directly.
A viewport instead of a clip path
In the app, the rectangle list’s clip-path reference didn’t hold, and its rows slid past the icon’s outline. That list and the invoice crop with a nested <svg> whose viewBox matches its own box, which needs no reference.
<svg x="32" y="64" width="512" height="384"
viewBox="32 64 512 384" overflow="hidden">…</svg>
The viewport only crops rectangles, so the building’s arched door keeps a clip path. The checklist keeps a rectangular one, widened so it doesn’t cut strokes at the edges.
A sidebar gets hovered all day
Emil Kowalski says UI animations should generally stay under 300 ms and weighs how often people will see one. Five of these run 1.28 to 1.37 s. Each plays once per hover and ignores a second hover mid-run, and on this page reduced motion turns them off.
After a stand-up discussion, I closed the ticket as not a fit for the app’s users, and the change sits unmerged.
My favorite part of the work is the feel of it, the small moments most people ship as an afterthought.