Accessibility
Two decisions, both made so you never have to combine flags: an icon is decorative or semantic, and reduced motion means different things on an icon than on a morph.
One rule, not a matrix
You never coordinate aria-hidden with aria-label. You either set label, or you don't:
| What you write | What lands in the DOM | When |
|---|---|---|
no label | aria-hidden="true" | The icon sits next to text that already says the same thing. |
label="Save" | aria-hidden="false" + aria-label="Save" | The icon is the content: a button with no text. |
<!-- Decorative: the button already says "Save" -->
<button><gf-icon [iconDef]="save" /> Save</button>
<!-- Semantic: no text, the icon carries the meaning -->
<button><gf-icon [iconDef]="save" label="Save" /></button>decorative defaults to true and you almost never touch it: label is what decides. Applies the same way to <gf-icon> and <gf-icon-morph>.
prefers-reduced-motion
Both components respect it by default (respectReducedMotion = true). But respecting it doesn't mean the same thing in both, and that difference is deliberate, not an inheritance oversight:
In <gf-icon>: it stays still
The choreography is decoration on top of an icon that's already the right one. Removing the motion doesn't remove information: the bell is still a bell.
In <gf-icon-morph>: it jumps to the new icon
Here, staying still would leave the user looking at the wrong icon. The destination isn't decoration, it's state: if the button went from "play" to "pause", it has to show "pause".
Reduced motion is respected by removing the movement, not the state change. The morph skips the transition and paints the destination immediately.
If you need to ignore the system preference — a rare case, and justifying it is on you — the override exists: [respectReducedMotion]="false".
What the component doesn't decide for you
- Color. The SVG uses
stroke="currentColor", so it inherits the text color. Contrast against the background is on you. - Touch target.
sizeis the size of the drawing. An icon button needs its own padding to reach the 24×24 CSS px minimum target. - Focus. The icon isn't interactive: it never receives focus or keyboard input. If it sits inside something clickable, that something has to be a real
<button>or<a>.
How the catalog’s motion is kept cheap
Every curated choreography is analysed by a deterministic linter — the same Motion Inspector you can open on any icon in the catalog, running as a CI check over the whole curated set on any PR that touches it. One implementation, two places.
It reports four things, measured over 1767 icons and 4795 variants:
- Animating
d— this fails the build. Changing the path data redraws it every frame;transformandopacityare moved by the compositor without repainting. The catalog has zero of these today, so the rule guards against a regression rather than flagging existing debt. Progressive stroke (autoDraw) is exempt: there is no cheap equivalent for it. - Duration over 1200 ms — a warning. 54 variants exceed it, mostly icons that tell a small story (dice rolling, weather). Loops are exempt by name (
idle,wander) and so are the cyclical defaults — a drifting cloud is slow on purpose. - Rotation over 720° — a warning. Four:
orbitandcalendar-cog/spinat 1080°, pluscloud-sync/holdandcloud-backup/holdat 7200°. Those two are not a frenzy: they are a continuous spin written as one long finite rotation (20 turns over 24 s, about 300°/s) that reverses when the pointer leaves. - A held pose under 0.8 units — a warning. A
holdretains its last keyframe, so that pose is the variant; below that threshold it cannot be told apart from rest. Eight remain, all of them icons whose geometry leaves no room to grow inside the canvas.
Only the first one breaks the build. The other two measure cost, not correctness, and an expensive choreography can be the right call for a given icon — a check that fails on a matter of taste gets switched off within a week.