गतिः · ANGULAR 22+

@code_with_sachin/ngx-motion

motion.dev directives — scroll reveals, hover springs, press feedback.

INSTALL

BASH
npm i @code_with_sachin/ngx-motion motion

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.
  • Reduced motion respected everywhere, configurably.
  • Listeners and observers torn down through DestroyRef.

Requirements

TESTED AGAINST

NAMETYPEDEFAULTNOTES
@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'.

TS · app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideMotion } from '@code_with_sachin/ngx-motion';

export const appConfig: ApplicationConfig = {
  providers: [
    provideMotion(),
  ],
};

// main.ts
// bootstrapApplication(App, appConfig);

Using NgModules

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

NAMETYPEDEFAULTNOTES
preset'fade' | 'fade-up' | 'slide-left' | 'slide-right' 'fade-up' Which entrance to play. slide-right enters from the left.
onceboolean true false re-hides the host when it leaves the viewport, so the reveal replays.
delaynumber 0 Seconds before the animation starts.
distancenumber 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.

स्पर्श

Default lift

scale 1.03 · lift -2

उत्थान

Taller lift

scale 1.05 · lift -8

विस्तार

Scale only

scale 1.08 · lift 0

HTML
<a ngxHoverSpring [scale]="1.05" [lift]="-4">Contact row</a>
<button ngxPress [depth]="0.94">Tap me</button>

NGXHOVERSPRING INPUTS

NAMETYPEDEFAULTNOTES
scalenumber 1.03 Scale at rest on hover.
liftnumber -2 Vertical offset in pixels; negative lifts.

NGXPRESS INPUTS

NAMETYPEDEFAULTNOTES
depthnumber 0.97 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

NAMETYPEDEFAULTNOTES
isBrowserbooleanFalse during SSR; every method below is a no-op then.
reducedbooleanTrue on the server, when config forces it, or when the OS asks for reduced motion.
animatetypeof import('motion').animateSSR-safe proxy. Returns a stub with stop/cancel/finished on the server.
inViewtypeof import('motion').inViewSSR-safe proxy. Returns a no-op stop function on the server.
scrolltypeof import('motion').scrollSSR-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.

HTML
<!-- 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 });
  }
}

Built for sachinsingh.me — this package ships from that portfolio's own workspace.

MIT · Sachin Singh