Getting started

Animated Lucide icons on native Web Animations API. Zero animation dependencies at runtime — no @angular/animations, no Motion, no GSAP.

Install

npm i glyphflow

Angular 20, 21, or 22 (peerDependencies: >=20.0.0 <23.0.0). The only real dependency is tslib. MIT licensed.

Your first icon

The component is standalone: import it where you use it, no intermediate module. The icon is passed as a value, via a named import.

import { Component } from '@angular/core';
import { GfIconComponent, bellIcon } from 'glyphflow';

@Component({
  selector: 'app-alert',
  imports: [GfIconComponent],
  template: '<gf-icon [iconDef]="bell" [size]="24" label="Notifications" />',
})
export class Alert {
  protected readonly bell = bellIcon;
}

Why [iconDef] and not name=

Because it's the path that actually tree-shakes. The numbers come from the budget that runs in CI on every commit, not a promise:

What you importBundle (gzip)
Just the component, no icon at all5.24 KB
Component + 1 icon via named import recommended5.6 KB
name= with the full registry174.74 KB

An icon costs that icon. Looking one up by name at runtime forces the bundler to keep all 1767, because it can't know which one you'll ask for.

If you still want name=

It exists, and it's opt-in on purpose: without registering the catalog, name="bell" renders nothing and doesn't throw — same contract as an unknown name.

import { provideIconCatalog, ANIMATED_ICONS } from 'glyphflow';

bootstrapApplication(App, {
  providers: [provideIconCatalog(ANIMATED_ICONS)],
});

When it animates

triggerWhat it does
groupDraws on mount and animates with the hover of the nearest .group container — the same one Tailwind's group-hover: uses. If there's none, it listens to the icon itself.
hoverOn pointer enter.
tapOn click.
viewOn entering the viewport. viewOnce decides once vs. every re-entry.
autoAs soon as it mounts.
manualNever on its own: you trigger it by calling play() on the instance.

Global speed

One multiplier for every computed duration — choreography and morph transitions alike. 1 means no change; without providing anything, behavior stays untouched.

import { provideGfIcons } from 'glyphflow';

providers: [provideGfIcons({ durationScale: 0.8 })]

The speed chips in this site's top bar are exactly that, live. They apply starting with the next playback: whatever's already running keeps the duration the browser already gave it.

Morphing between two icons

Lives in a separate entry point, glyphflow/morph, so that 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] triggers the transition from the previous value.
// <gf-icon-morph [icon]="current()" [size]="32" label="Status" />

The first value renders static — morphing "from nothing" doesn't exist. SSR and any browser without WAAPI fall into the same case: you see the icon, it just doesn't animate.

[spring] decides what the transition feels like: smooth (default, no overshoot), snappy (fast, subtle overshoot), or bouncy (overshoots the target and settles back). The spring never touches the geometry — only the duration and how keyframes are spaced out over time. It also accepts your own { k, c } if you want to tune it yourself.