Declarative motion for Angular 21 β presets, spring physics, drag, scroll, parallax,
presence orchestration & SVG path-drawing. SSR-safe. Zero @angular/animations. Standalone-ready.
π Live Demo Β Β·Β π Docs Β Β·Β π¦ npm Β Β·Β πΊοΈ Roadmap
UI animation in Angular tends to sprawl: enter/leave transitions rewritten per component, imperative logic tangled into templates, inconsistent timings across a team, and no clean way to orchestrate staggered lists or exit animations.
Angular Movement replaces that boilerplate with declarative directives and one global config,
so motion stays consistent, composable, and SSR-safe. Playback runs on the browser's native
Web Animations API (with an optional spring physics engine) β no @angular/animations setup required.
<h2 [move]="'fade-up'">Hello movement</h2>
<button [moveWhileHover]="{ scale: [1, 1.05] }">Hover me</button>| π¬ 30+ presets | fade, slide, zoom, flip, blur, bounce, pulse, spin, icon-draw/pulse/bounce |
| 𧬠Custom keyframes | full control when a preset isn't enough |
| π Spring physics | pre-computed spring keyframes via a dedicated engine |
| π±οΈ Interactions | hover, tap, focus, in-view, scroll, parallax, drag |
| π― Advanced drag | axis-lock, constraints, elasticity, momentum, snap-to-origin & snap points |
| π» Presence | let leave animations finish before the DOM node is removed |
| πͺ Stagger | coordinated, ordered list motion |
| βοΈ SVG path drawing | animate pathLength / pathOffset, WAAPI-powered |
| β±οΈ Per-property transitions | different duration / delay per animated property |
| π Motion values | derive motion from Angular signals (moveValue, moveTransform, moveSpringValue) |
| π₯οΈ SSR-safe | every browser API guarded; no-ops on the server |
| π§± Standalone-ready | tree-shakeable directives, no NgModule required |
npm install angular-movement
# or: pnpm add angular-movement Β· yarn add angular-movementPeer dependencies:
@angular/core^21.2.0 Β·@angular/common^21.2.0
1. Provide global defaults
import { ApplicationConfig } from '@angular/core';
import { provideMovement } from 'angular-movement';
export const appConfig: ApplicationConfig = {
providers: [
provideMovement({
duration: 320,
easing: 'cubic-bezier(0.16, 1, 0.3, 1)',
delay: 0,
disabled: false,
}),
],
};2. Import the directives and use them in templates
import { Component } from '@angular/core';
import { MOVEMENT_DIRECTIVES } from 'angular-movement';
@Component({
selector: 'app-demo-card',
standalone: true,
imports: [...MOVEMENT_DIRECTIVES],
template: `
<h2 [move]="'fade-up'">Hello movement</h2>
<button [moveWhileHover]="{ scale: [1, 1.05] }">Hover me</button>
`,
})
export class DemoCardComponent {}Start with the smallest primitive that matches the job:
| Level | Reach for |
|---|---|
| Basic | moveEnter, moveLeave, [move], moveInitial, moveAnimate, moveExit |
| Interactions | moveWhileHover, moveWhileTap, moveFocus, moveInView |
| State | moveVariants, moveTarget, moveTrigger |
| Orchestration | movePresence, moveStagger |
| Scroll & layout | moveScroll, moveParallax, moveLayout, moveSmoothScroll |
| Advanced | pathLength, pathOffset, transition, spring, moveDrag |
moveLeaveplays only while a parentmovePresencekeeps the view alive during removal. A plain@ifremoves the element immediately, so there is no node left to animate.
Every directive has a focused page with a live config panel and copy-paste HTML output.
Demo pages: Animate Β· Enter & Leave Β· Hover & Tap Β· In-View Β· Scroll & Parallax Β· Presence Β· Layout Β· Drag Β· Variants Β· Text Β· SVG Icons
Motion-style API β initial / animate / exit
<ng-container *movePresence="isOpen">
<article
[moveInitial]="{ opacity: 0, y: 24 }"
[moveAnimate]="{ opacity: 1, y: 0 }"
[moveExit]="{ opacity: 0, y: -16 }"
moveDuration="300"
>
Card
</article>
</ng-container>The object form [moveAnimation]="{ initial, animate, exit }" is also available for config-heavy cases.
Drag gestures β constraints, momentum, snap points
<div
moveDrag="x"
[moveDragConstraints]="{ left: -120, right: 120 }"
[moveDragElastic]="0.35"
[moveDragMomentum]="true"
[moveDragSnapPoints]="[{ x: -120, y: 0 }, { x: 0, y: 0 }, { x: 120, y: 0 }]"
(moveDragStart)="onDragStart($event)"
(moveDragMove)="onDragMove($event)"
(moveDragEnd)="onDragEnd($event)"
>
Drag me
</div>Use moveWhileTap for press feedback that returns on release; use moveDrag when the element
should follow the pointer and keep a real position.
SVG path drawing & icon helpers
Animate pathLength from 0 to 1 to draw a stroke. The engine measures the element's total length
and converts it to WAAPI-compatible strokeDasharray / strokeDashoffset keyframes.
<svg width="24" height="24" viewBox="0 0 24 24">
<path
[moveTarget]="animate()"
[moveFrames]="{ pathLength: [0, 1], opacity: [0, 1] }"
moveDuration="700"
fill="none"
stroke="currentColor"
stroke-width="2"
d="M4 12l4-4 4 4 8-8"
/>
</svg>Helper functions build icon keyframes quickly:
import { movePathDraw, moveIconPulse } from 'angular-movement';<svg [moveTarget]="animate()" movePreset="icon-bounce" moveDuration="500">
<!-- icon paths -->
</svg>Variants with per-property transitions
Declare target states like Framer Motion. When moveAnimate changes, keyframes are generated from
the previous state to the next.
<div
[moveVariants]="{
idle: { scale: 1, rotate: 0 },
active: { scale: 1.08, rotate: 4 }
}"
[moveAnimate]="isActive ? 'active' : 'idle'"
>
Card
</div>Override timing per property, and point moveExitVariant at the variant that plays before removal:
<ng-container *movePresence="isOpen">
<aside
[moveVariants]="{
visible: { opacity: 1, x: 0 },
hidden: { opacity: 0, x: 24 }
}"
moveAnimate="visible"
moveExitVariant="hidden"
>
Panel
</aside>
</ng-container>Motion values driven by signals
import { computed, inject, Injector } from '@angular/core';
import { moveSpringValue, moveTransform, moveValue } from 'angular-movement';
const progress = moveValue(0);
const x = moveTransform(progress, [0, 1], [0, 120]);
const scale = moveSpringValue(moveTransform(progress, [0, 1], [0.9, 1]), {
injector: inject(Injector),
});
const transform = computed(() => `translateX(${x()}px) scale(${scale()})`);Scroll directives expose progress as a signal, so you can derive values without a manual scroll loop:
<section
#scroll="moveScroll"
[moveScroll]="{ opacity: [0, 1] }"
[style.--progress]="scroll.progress()"
>
Scroll-linked content
</section>This is a pnpm monorepo with two parts:
| Path | What |
|---|---|
projects/movement |
The publishable npm library (angular-movement) |
src |
Demo & documentation site β AnalogJS (Vite + SSR, zoneless) |
The demo site imports the library via a Vite path alias, so library changes are reflected live without a build step.
pnpm dev # run the demo site
pnpm test # library unit tests (Vitest)
ng build movement # build the library β dist/movement
pnpm build # build the demo site (client + SSR)The demo site deploys to Cloudflare Pages β a single source of truth for hosting.
- Automatic: every push to
mainruns.github/workflows/deploy-cloudflare.yml. - Manual:
pnpm deploybuilds and shipsdist/analog/publicvia Wrangler.
Live at angular-movement.andersseen.dev.
Contributions are welcome through issues and pull requests. When proposing changes, include a problem statement, any public-API impact, and tests or demo updates for new behavior.
- π Contributing guide
- π€ Code of conduct
- π Security policy
- πΊοΈ Roadmap
- β Release checklist
MIT Β© Andersseen