Lucide icons that actually move, for Angular — per-shape choreography on the
native Web Animations API. No @angular/animations, no third-party animation library, nothing to
configure. Plus morphing between icons, in its own entry point.
Free, MIT, no backend. See NOTICE for third-party attribution.
npm i glyphflowMeasured in CI on every push, not promised — npm run bundle-check:
| What you import | gzip |
|---|---|
| The component alone, no icons | 5.50 KB |
One icon ([iconDef]="bellIcon") |
5.87 KB |
The whole catalog (name="bell") |
174.74 KB |
An icon costs about 0.36 KB on top of the runtime. Looking one up by name forces the bundler to keep
all 1767, because it cannot know which one you will ask for — so that route is opt-in via
provideIconCatalog(ANIMATED_ICONS), never a hidden fallback.
import { GfIconComponent, bellIcon } from 'glyphflow';<gf-icon [iconDef]="bellIcon" trigger="hover" /> <gf-icon [iconDef]="searchIcon" label="Search" />trigger |
Fires |
|---|---|
group (default) |
Draws on mount, animates on the nearest .group hover — the same ancestor Tailwind's group-hover: reads. Falls back to its own hover with no .group around. |
hover |
Pointer enters. |
tap |
Click. |
view |
Enters the viewport. viewOnce (default true) decides once vs. every re-entry. |
auto |
On mount. |
manual |
Never on its own. |
manual pairs with three public methods — grab the instance with ViewChild/viewChild():
@ViewChild(GfIconComponent) icon!: GfIconComponent;
this.icon.play('pulse'); // plays a VARIANT — not a trigger; no argument replays `animation`
this.icon.reverse(); // reverses the animation currently running
this.icon.cancel(); // stops it and resets to the base poseA touch screen has no pointer passing over things, so hover-driven icons (group, hover) only
draw on mount there. That is on purpose: a tap fires a synthetic pointerenter, and animating on
it would swallow the feel of the tap. The touch input is the opt-in:
touch |
On a touch screen |
|---|---|
none (default) |
Nothing new — the behavior above. |
press |
Pressing with a finger plays the hover variant; lifting it reverses it when the variant has reverseOnLeave. For buttons and controls. |
view |
Only on screens without hover ((hover: none)): plays the hover variant when the icon enters the viewport. For grids and lists. |
<button type="button"><gf-icon [iconDef]="bellIcon" touch="press" /> Notify me</button>It only adds listeners: mouse hover keeps working exactly as before, and tap, view, auto and
manual ignore it. Set it once for the whole app with provideGfIcons({ touch: 'press' }); an
icon's own touch input wins over the global one.
Inside a web component whose real <button> lives in its shadow root — <ion-button>,
<ion-tab-button>, a clickable <ion-item>, or any other — the icon hooks onto the component
itself, so hover and press count over the whole button, not just over the icon.
| Total icons | 1767 — the complete canonical Lucide 1.31 set |
| Hand-choreographed | 1767 — every one, motion with intent |
| More than one gesture | 1095 — a second variant beyond default |
Every icon in the catalog is now hand-choreographed: generated-icons.ts is empty, and stays as
the landing zone for whatever a future Lucide release adds. The two files exist on purpose
(curated-icons.ts and generated-icons.ts): the generator never touches the curated ones.
Choreography with intent is human judgement, icon by icon. See CONTRIBUTING.md.
You never coordinate aria-hidden with aria-label. You either pass a label or you don't:
<!-- Decorative: the button already says "Save" -->
<button><gf-icon [iconDef]="saveIcon" /> Save</button>
<!-- Semantic: no text, so the icon carries the meaning -->
<button><gf-icon [iconDef]="saveIcon" label="Save" /></button>Both components honour prefers-reduced-motion by default — but honouring it means something
different in each, and that is deliberate. In <gf-icon> the choreography is decoration on an icon
that is already correct, so it holds still. In <gf-icon-morph> holding still would leave you
staring at the wrong icon, so it jumps straight to the destination. Reduced motion removes the
motion, not the state change.
Lives in a separate entry point, glyphflow/morph, so anyone who only wants animated icons doesn't
pay for shape-interpolation math.
import { GfIconMorphComponent } from 'glyphflow/morph';
// The binding IS the state: changing [icon] transitions from the previous value.The first value renders statically — morphing "from nothing" doesn't exist. SSR and browsers without WAAPI land there too: you see the icon, it just doesn't animate.
import { Component } from '@angular/core';
import { GfIconMorphComponent } from 'glyphflow/morph';
import { moonIcon, sunIcon } from 'glyphflow';
@Component({
imports: [GfIconMorphComponent],
template: `
<button (click)="dark = !dark">
<gf-icon-morph [icon]="dark ? moonIcon : sunIcon" />
</button>
`,
})
export class ThemeToggle {
dark = false;
}One multiplier for every calculated duration — choreography and morph transitions alike:
import { provideGfIcons } from 'glyphflow';
providers: [provideGfIcons({ durationScale: 0.8 })]; // 20% faster, everywhere1 is the default — no change. It applies from the next playback: anything already animating keeps
the duration WAAPI already received when it started.
Angular 20, 21 and 22 — each version is tested in CI by scaffolding a fresh app with that
version's CLI, installing the real .tgz, and building it with SSR prerendering. The
peerDependencies range reflects only what that matrix confirmed.
projects/glyphflow— the library (ng-packagr,gfprefix →<gf-icon>).projects/playground— the live site: icon catalog, patterns, geometry editor and authoring lab. It consumes the published package from npm, never the source in this repo, so that anything the public API fails to cover shows up immediately.
npm install
ng build glyphflow
ng test glyphflow
ng serve playground
npm run verify:clean # wipes node_modules/dist and runs the whole pipeline from scratch
npm run generate:icons # regenerates generated-icons.ts from lucide-static
npm run lucide:diff # diffs the catalog against the installed Lucide versionMIT — see LICENSE.
