motion is the animation runtime — declared as peer dependencies, so you control the version.
Why
motion.dev gives you a tiny WAAPI-backed animation runtime; this package gives it Angular manners. Three attribute directives cover the micro-interaction layer — entrance reveals, hover springs, press feedback — and one injectable service wraps the raw API so it no-ops on the server instead of reaching for document.
SSR-safe by construction — all DOM work runs inside afterNextRender.
Listeners and observers torn down through DestroyRef.
Requirements
TESTED AGAINST
NAME
TYPE
DEFAULT
NOTES
@angular/core
^22.0.0
22.0.5
Standalone APIs, signal inputs and afterNextRender are all required.
typescript
~6.0.0
6.0.3
Whatever your Angular version supports.
node
>=20
24.15.0
Build and SSR only.
motion
^12.0.0
12.42.2
The animation runtime. Every directive proxies through it.
Peer ranges are wider than this — the table lists the exact versions the demos on this page are running, so you have a known-good combination to fall back on.
Providers
Optional — provideMotion() goes in your application config. Omit it entirely and every directive still works, defaulting to reducedMotion: 'user'.
Everything here is standalone, but standalone components and directives are importable from an @NgModule — put them in the module's imports, not declarations. No importProvidersFrom is needed: @NgModule.providers is typed Array<Provider | EnvironmentProviders>, so the provide*() functions drop straight in.
TS · app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { InViewDirective, HoverSpringDirective, PressDirective, provideMotion } from '@code_with_sachin/ngx-motion';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
// Standalone components and directives go in `imports`.
imports: [
BrowserModule,
InViewDirective,
HoverSpringDirective,
PressDirective,
],
providers: [provideMotion()],
bootstrap: [AppComponent],
})
export class AppModule {}
ngxInView
Plays an entrance when the host scrolls into view. The demo remounts the row so you can watch it again.
प्रकटनम् — revealed with preset="fade-up"
TS
import { InViewDirective } from '@code_with_sachin/ngx-motion';
@Component({
imports: [InViewDirective],
template: `
<section ngxInView>Fades up when scrolled into view</section>
<img ngxInView preset="slide-left" [delay]="0.15" [distance]="40" />
<!-- once=false re-hides the element when it leaves the viewport -->
<p ngxInView preset="fade" [once]="false">Replays every pass</p>
`,
})
export class Section {}
INPUTS
NAME
TYPE
DEFAULT
NOTES
preset
'fade' | 'fade-up' | 'slide-left' | 'slide-right'
'fade-up'
Which entrance to play. slide-right enters from the left.
once
boolean
true
false re-hides the host when it leaves the viewport, so the reveal replays.
delay
number
0
Seconds before the animation starts.
distance
number
28
Travel in pixels for the sliding presets.
ngxHoverSpring & ngxPress
Hover the cards; press and hold the button. Both use a critically-damped bezier via WAAPI, so there is no layout thrash and no rAF loop of your own.
Scale while the pointer is down. Released on up, cancel and leave.
MotionService
An SSR-safe facade over motion.dev's animate, inView and scroll. Inject it when a directive is not enough — the guards are already handled.
isBrowser · false reduced · true
Live from this page — flip your OS "reduce motion" setting and reload to see it change.
TS
import { MotionService } from '@code_with_sachin/ngx-motion';
export class Hero {
private readonly motion = inject(MotionService);
reveal(el: HTMLElement) {
// No-ops on the server, so this is safe to call anywhere.
if (this.motion.reduced) return;
this.motion.animate(el, { opacity: [0, 1], y: [24, 0] }, { duration: 0.6 });
}
track(el: HTMLElement) {
// inView and scroll are proxied the same way.
return this.motion.inView(el, () => console.log('entered'));
}
}
MEMBERS
NAME
TYPE
DEFAULT
NOTES
isBrowser
boolean
—
False during SSR; every method below is a no-op then.
reduced
boolean
—
True on the server, when config forces it, or when the OS asks for reduced motion.
animate
typeof import('motion').animate
—
SSR-safe proxy. Returns a stub with stop/cancel/finished on the server.
inView
typeof import('motion').inView
—
SSR-safe proxy. Returns a no-op stop function on the server.
scroll
typeof import('motion').scroll
—
SSR-safe proxy for scroll-linked animation.
Recipes
The three directives cover most of a page's micro-interactions. These are the combinations worth stealing.
Staggered grid reveal
NGXINVIEW
Delay by index and a grid becomes a cascade. Each host animates itself when it enters, so there is nothing to orchestrate and nothing to keep in sync.
वीर
DELAY 0MS
शान्त
DELAY 80MS
रौद्र
DELAY 160MS
हास्य
DELAY 240MS
अद्भुत
DELAY 320MS
करुण
DELAY 400MS
HTML
<!-- Delay by index and a grid becomes a cascade.
No wrapper component, no orchestration — each host animates itself. -->
@for (card of cards; track card.id; let i = $index) {
<article ngxInView preset="fade-up" [delay]="i * 0.08">
{{ card.title }}
</article>
}
Replay on every pass
NGXINVIEW
once=false re-hides the host when it leaves the viewport. Good for a long page where a reader scrolls back up; bad for anything they need to read.
Scroll me out of this box and back — the reveal runs again.
HTML
<!-- once=false re-hides the host on exit, so the reveal
replays every time it passes through the viewport. -->
<p ngxInView preset="fade" [once]="false">Replays on every pass</p>
Contact rows
NGXHOVERSPRING
A small scale plus a lift of a few pixels is enough. Anything larger reads as a bounce and starts fighting the pointer.
<!-- Hover springs read best on rows a pointer travels down. -->
@for (row of contacts; track row.label) {
<a ngxHoverSpring [scale]="1.02" [lift]="-3" class="flex justify-between">
<span>{{ row.label }}</span>
<span>{{ row.value }}</span>
</a>
}
Hover and press together
NGXHOVERSPRING + NGXPRESS
Attribute directives compose on one host: the spring owns hover, the press owns pointer-down. They animate the same transform without fighting, because both go through WAAPI.
HTML
<!-- Directives compose: spring on hover, depress on pointer-down. -->
<button ngxHoverSpring ngxPress [scale]="1.06" [lift]="-3" [depth]="0.92">
Hire once
</button>
Imperative animation
MOTIONSERVICE
When a directive is not enough, inject the service. Keyframe arrays behave exactly as in motion.dev, and the SSR guard is already handled for you.
शक्ति
TS
import { MotionService } from '@code_with_sachin/ngx-motion';
export class Panel {
private readonly motion = inject(MotionService);
// Keyframe arrays work exactly as they do in motion.dev.
pop(el: HTMLElement) {
this.motion.animate(el,
{ scale: [1, 1.25, 0.95, 1] },
{ duration: 0.5, ease: [0.22, 1, 0.36, 1] });
}
shake(el: HTMLElement) {
this.motion.animate(el, { x: [0, -8, 8, -5, 5, 0] }, { duration: 0.4 });
}
}