Declarative, signals-native motion for Angular 21 and 22 β presets, variants, gestures, spring
physics, drag, scroll-linked animation, layout transitions, presence orchestration, motion values &
SVG path-drawing. SSR-safe, zoneless-ready. Zero @angular/animations.
π 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; repeat / repeatType / repeatDelay |
| π 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 points & moveWhileDrag |
| π» Presence | leave animations finish before removal β for a single view or a keyed list |
| πͺ Stagger | ordered list motion, plus staggerChildren orchestration inside variants |
| βοΈ SVG path drawing | animate pathLength / pathOffset, WAAPI-powered |
| β±οΈ Per-property transitions | different duration, delay and easing per property; explicit keyframe times |
| π 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/coreand@angular/commonβ^21.2.0 || ^22.0.0. Every supported major is compiled against the packed package in CI (pnpm validate:consumer).
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 only the directives you use
Standalone components tree-shake per route, but only if you import what you actually use β a directive that's never imported anywhere never ships.
import { Component } from '@angular/core';
import { MoveAnimateDirective, MoveHoverDirective } from 'angular-movement';
@Component({
selector: 'app-demo-card',
standalone: true,
imports: [MoveAnimateDirective, MoveHoverDirective],
template: `
<h2 [move]="'fade-up'">Hello movement</h2>
<button [moveWhileHover]="{ scale: [1, 1.05] }">Hover me</button>
`,
})
export class DemoCardComponent {}
MOVEMENT_DIRECTIVES(all 21, spread intoimports) is still exported as a convenience for prototyping or a component that genuinely uses most of the library β just know it pulls in everything, including directives that component doesn't use and every experimental one (moveLayout,moveDrag,moveSmoothScroll,moveTarget,moveTrigger). If you want a spread that can never silently start pulling in an experimental directive, useMOVEMENT_STABLE_DIRECTIVESinstead (there's also a standaloneMOVEMENT_EXPERIMENTAL_DIRECTIVESfor the other five).
Start with the smallest primitive that matches the job:
| Level | Reach for |
|---|---|
| Basic | moveEnter, moveLeave, [move], moveInitial, moveAnimate, moveExit |
| Interactions | moveWhileHover, moveWhileTap, moveWhileFocus, 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.
Several of these look interchangeable at first glance. They're not β each covers a distinct job:
[move]/moveAnimatevs[moveAnimation]. Both describe a single element's own enter/leave.[move]/moveAnimatetake a preset name orMoveKeyframespairs ({ opacity: [0, 1] });[moveAnimation]takes Framer-style single-value state objects ({ initial, animate, exit }) and is reactive toanimatechanging. Reach for[moveAnimation]when you're already thinking ininitial/animate/exitstate, otherwise[move]is the simpler default.moveVariantsvsmoveTarget/moveTrigger.moveVariantspropagates a named state down through DI to nested[moveVariants]children β the tool for a shared state (idle,open,active) driving a subtree, withstaggerChildren/delayChildren/whenorchestration.moveTarget/moveTrigger(experimental) instead connect two elements that do not share a parent β a boolean signal flips an animation on a target elsewhere in the DOM, with no DI propagation. PrefermoveVariantswhenever the elements involved share an ancestor.moveStaggervsstaggerChildren.[moveStagger]delays its direct animated children in DOM order β the tool for a flat list.staggerChildren(amoveVariantsstate property) staggers nested[moveVariants]subtrees on a variant change β the tool when the staggered items are themselves stateful, not just entering once.
Every directive has a focused page with a live config panel and copy-paste HTML output.
Demo pages: Animate Β· Animation (object API) Β· Enter & Leave Β· Hover & Tap Β· Focus Β· 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. Use moveVariant to set the active state;
moveActiveVariant is a permanent, fully-supported alias for the same input (@deprecated only to
signal which name to prefer β it is not going away). When the active variant changes, keyframes
are generated from the previous state to the next.
<div
[moveVariants]="{
idle: { scale: 1, rotate: 0 },
active: { scale: 1.08, rotate: 4 }
}"
[moveVariant]="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 }
}"
moveVariant="visible"
moveExitVariant="hidden"
>
Panel
</aside>
</ng-container>Motion values driven by signals
Called from a field initializer or constructor of a class Angular itself constructs β a
component, directive, or service β moveSpringValue infers its injector automatically, the same
convention toSignal/toObservable use:
import { Component, computed } from '@angular/core';
import { moveSpringValue, moveTransform, moveValue } from 'angular-movement';
@Component({ selector: 'app-card', template: `...` })
class CardComponent {
progress = moveValue(0);
x = moveTransform(this.progress, [0, 1], [0, 120]);
scale = moveSpringValue(moveTransform(this.progress, [0, 1], [0.9, 1]));
transform = computed(() => `translateX(${this.x()}px) scale(${this.scale()})`);
}Calling it from outside an injection context (a plain function invoked later, a different
injector than the surrounding one) still needs an explicit injector:
import { inject, Injector } from '@angular/core';
import { moveSpringValue } from 'angular-movement';
function buildScale(source: Signal<number>, injector: Injector) {
return moveSpringValue(source, { injector });
}moveSpringValue also respects prefers-reduced-motion automatically, same as every directive β
under reduced motion it jumps straight to the target value instead of animating.
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>| Status | APIs |
|---|---|
| Stable | provideMovement, MOVEMENT_DIRECTIVES, MOVEMENT_STABLE_DIRECTIVES, [move], [moveAnimate], moveEnter, moveLeave, *movePresence, moveStagger, moveWhileHover, moveWhileTap, moveWhileFocus, moveInView, moveScroll, moveParallax, [moveAnimation], *movePresenceFor, moveVariants, moveText, moveLoop, MoveAnimator, moveValue, moveTransform, moveSpringValue, the preset library (MOVE_PRESETS and the icon helpers) |
| Stable candidate | (none currently β the 1.0 freeze pass promoted every candidate; new APIs may land here first) |
| Experimental | MOVEMENT_EXPERIMENTAL_DIRECTIVES, moveLayout, moveDrag (the whole directive β constraints, momentum, snap points, moveWhileDrag), moveSmoothScroll / SmoothScrollService, moveTarget, moveTrigger |
MOVEMENT_DIRECTIVES itself is stable β spreading it always compiles and its own shape follows
SemVer β but its contents are not stability-pure: it includes all five experimental directives
above. Use MOVEMENT_STABLE_DIRECTIVES instead if you want a spread that can never silently start
pulling in an experimental directive, or MOVEMENT_EXPERIMENTAL_DIRECTIVES for just those five.
Stable APIs follow semantic-versioning expectations. Candidate APIs are feature-complete but may
receive small naming or behavior adjustments. Experimental APIs can change significantly between
minor versions β see the versioning policy below for exactly what that means going into 1.x.
Every exported type mirrors the stability of the API it supports β MoveKeyframes is stable
because the directives that take it are stable, MoveDragEvent is experimental because
moveDrag is. AnimationControls (the return type shared by MoveAnimator and every directive
internally) and the MovementConfig family (behind provideMovement) are stable on their own:
their shape hasn't changed since 0.5 and both are load-bearing for everything else.
moveActiveVariant is @deprecated in favor of moveVariant (same value, one name) but stays a
permanent, fully-supported alias β it will not be removed without a major version.
Each level is also declared in the source as a @stability JSDoc tag (stable / candidate /
experimental), so your editor shows the guarantee at the point of use β check the tag on the
specific declaration you're using for the authoritative answer. Experimental declarations
additionally carry the standard @experimental tag.
There is no secondary angular-movement/experimental entry point β every experimental export
ships from the same package. This is the one deliberate exception to normal SemVer:
- Experimental exports may change or be removed in any
1.xminor, including breaking changes to inputs, outputs, or behavior β the same convention Angular CDK uses for its own experimental APIs. - Every experimental-only breaking change is called out under its own
### Changed (experimental)CHANGELOG heading, separate from the normal### Changed, so you can safely ignore it if you don't use experimental APIs. - Where practical, an experimental API gets a deprecation warning (dev-mode console warning or
@deprecatedtag) for at least one minor version before removal.
If you only use [move], moveVariants, *movePresenceFor, moveScroll, and the rest of the
Stable row above, normal SemVer applies to your app without exception.
This "no secondary entry point" decision was reaffirmed in the post-1.0 hardening pass (spec 013): the experimental surface still needs no dependency stable consumers shouldn't pay for, so splitting the package would be migration churn with no architectural win. It remains open for a future minor if a concrete reason appears.
Two deliberate groups, frozen for 1.0:
- Reactive β changing an input while the directive is alive updates or replays the animation:
moveWhileHover,moveWhileTap,moveWhileFocus,moveVariants,moveTarget,moveTrigger,moveScroll,moveParallax,moveDrag,moveLoop,moveText, and[moveAnimation]'sanimatestate. - One-shot by design β these describe a single entrance or exit, so they play once and ignore
later input changes:
moveAnimate/[move],moveEnter,moveLeave,moveInView,moveSmoothScroll. To play one again, wrap the element in*movePresence/*movePresenceForor re-create the view.
[moveAnimation] compares its animate state by value, so binding an object literal straight
in the template does not replay the animation on every change detection pass.
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