Animating Icons
An icon is not a picture you move. It is an object that does something, and the animation is that thing happening once.
Core principle: the gesture is the icon's meaning acting on itself. A bell rings, a plug disconnects, a package lands. The motion has to be something only that object could do. Whole-<svg> shakes that would look identical on any glyph are decoration — delete them.
Read next
The swap test
Could these exact keyframes be pasted onto a different icon and still look fine? If yes, it is decoration. Run twice: once on the idea, once on the finished icon.
| Fails | Passes |
|---|
Whole <svg> shakes / bounces / spins | One part moves against another part |
| Every icon in a batch got the same recipe | The recipe was chosen from the icon's verb |
| "Pulse" or "wiggle" applied to a noun | The object's own mechanism operates |
The four gates
Every icon passes all four. Verify by rendering — never by reasoning. Details in VERIFY.md.
- Swap test — gesture unique to this icon's meaning.
- Rest test — frame 0 and final frame are pixel-identical to the source glyph (difference overlay). Licensed exception: a deliberate redraw, stated out loud.
- Strip test — every intermediate frame still reads as the icon.
- Motion test — peak speed under the aliasing ceiling; min speed ~0 in any beat meant to stop; claimed ratios measured off the computed matrix.
Landing on rest
Gate 2 shapes the gesture. Name which landing you use before designing beats:
- Symmetry — end on a rotation/reflection that maps the drawing onto itself. Measure the symmetry, never count the lobes (optical adjustment often lowers the true order).
- Congruence — identical parts (up to translation) can end swapped; check absolute point sets, not command fingerprints.
- Re-arm while invisible — jump state outside the clip / behind a part / at
opacity: 0 / scale(0) on a hundredth-of-a-percent frame; prove invisibility by measurement.
- Return — go out and come back with a different character on the return leg (not a plain undo).
If you cannot name which of the four, you do not have a gesture yet.
Choose the mechanism
| What is happening | Reach for | Not |
|---|
| Part slides / turns / drops; shape unchanged | CSS transform on a wrapper <g> | a morph |
| Shape genuinely changes (paper bends, book closes) | d-morph (MORPH.md) | scaleY/skew fakery |
| Something goes inside / in front / is a hole | mask or clipPath | opacity; z-order |
| A line is written or erased | stroke-dashoffset | a fade |
| Any icon becomes any other in one UI slot | three-line morph (ICON-MORPH.md) | a crossfade |
Ask every time: is the object changing shape, or my view of a rigid object? Almost always the second → transform. Prefer a hard-swap at a covered instant over a morph when coverage is provable.
Procedure
- Read the geometry — open the source; count elements; find hinges, seams, symmetry. Hugeicons free is usually pre-split. Hand-drawn storyboards: classify the second pose (byte-identical / translated / fuller redraw) before designing — see MORPH.md.
- Name the verb — one sentence the object does ("a plug comes out"), not a shape description.
- Pick the one part that carries the verb; the rest holds still as the frame.
- Choose a family from FAMILIES.md. Do not invent a gesture per icon.
- Decide the landing (symmetry / congruence / re-arm / return) — it often sets the numbers.
- Derive every number from the geometry. Measure, do not taste. Dwells/decay ratios come from the budget table below.
- Write it — non-negotiable constraints:
- CSS keyframes, no JS animation library (exception: icon-swap / Motion).
- No layout triggers. Prefer
transform / opacity; stroke-dashoffset and discrete d / visibility OK. Never width/height/x/y/r/cx/cy.
- No
px on the root <svg> or HTML wrapper unless computed from size (TECHNIQUE.md § Units).
- Last keyframe is rest. Declare rest in a base rule, not only at
0% (FAILURES.md #2).
- Animation names prefixed
ig- so the driver and harness can count them.
- Verify — run VERIFY.md; second pass with fresh eyes.
The budget
Gesture — plays once on hover (library browse = rare):
| |
|---|
| Duration, ONE event | 600–1100ms |
| Duration, N countable beats | ~350–400ms per beat |
| Dwell before start | 130ms |
| Abandoned mid-flight | speed up ×2.2, never cancel |
Derive duration from the set you ship into, not vibe:
grep -oE 'animation: *ig-[a-z0-9-]+ +[0-9.]+s' . | grep -oE '[0-9.]+s' | sort -n | uniq -c
Beats below ~250ms blur into one event. Anticipation needs real time (~40ms+), not a percentage of a tiny beat.
Reduced — sidebar / 100+ hovers a day: 140–220ms, ≤1px translate, scale ≤1.10, rotate ≤16°, holds while hovered. Ask how often it is seen before picking a tier.
Playback contract
Play once on hover, guaranteed to finish. Drive off data-go + dwell, not bare :hover (TECHNIQUE.md § Driver). Honour prefers-reduced-motion: reduce with animation: none (safe only because of gate 2). Icon-swap under reduce: instant coordinate set, no tween.
Done checklist
Full gate list: VERIFY.md.
Red flags — stop and re-derive
- Keyframes written before opening the source
d
- Cannot name the landing
- Curve chosen by name for a large turn without computing peak slope (EASING.md)
- Transform on the root
<svg>
- Two icons share keyframe values not derived from matching geometry
- A number ends in 0 or 5 with no geometric source
ease: linear on anything that is not a spin or marquee
- Looks right only because it is small
- Probe returned 0 and was not shown capable of returning non-zero