import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { LottieComponent, AnimationOptions } from 'ng-lottie';
@Component({
selector: 'app-motion-player',
standalone: true,
imports: [LottieComponent],
template: <ng-lottie [options]="animationConfig" [width]="displayWidth" [height]="displayHeight" class="motion-container" /> ,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MotionPlayerComponent {
@Input({ required: true }) sourcePath!: string;
@Input() loop = false;
@Input() autoPlay = true;
@Input() displayWidth = '100%';
@Input() displayHeight = '100%';
animationConfig: AnimationOptions;
constructor() {
// Configuration is derived from inputs
this.animationConfig = {
path: this.sourcePath,
loop: this.loop,
autoplay: this.autoPlay,
renderer: 'svg'
};
}
@Input('sourcePath')
set setSourcePath(path: string) {
this.sourcePath = path;
this.animationConfig.path = path;
}
}
**Rationale:**
- **Standalone:** Aligns with Angular 17+ standards, reducing module boilerplate.
- **OnPush:** Optimizes change detection; the animation state is managed by `lottie-web`, not Angular's zone.
- **Input Setters:** Allows dynamic path updates without recreating the component instance.
- **Renderer:** Defaults to `svg` for crisp rendering on high-DPI displays.
#### 3. Direct API Integration for Advanced Control
When you need frame-level manipulation, custom event handling, or performance tuning beyond the wrapper's scope, use `lottie-web` directly. This approach requires manual lifecycle management.
```typescript
import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import lottie from 'lottie-web';
@Component({
selector: 'app-direct-motion',
standalone: true,
template: `<div #hostRef class="motion-host"></div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DirectMotionComponent implements AfterViewInit, OnDestroy {
@ViewChild('hostRef', { static: true }) hostRef!: ElementRef<HTMLDivElement>;
private animationInstance: ReturnType<typeof lottie.loadAnimation> | null = null;
ngAfterViewInit(): void {
this.initializeAnimation();
}
private initializeAnimation(): void {
if (!this.hostRef) return;
this.animationInstance = lottie.loadAnimation({
container: this.hostRef.nativeElement,
renderer: 'svg',
loop: true,
autoplay: true,
path: 'assets/motion/complex-sequence.json'
});
}
ngOnDestroy(): void {
this.cleanup();
}
private cleanup(): void {
if (this.animationInstance) {
this.animationInstance.destroy();
this.animationInstance = null;
}
}
// Example of advanced control
public goToFrame(frameIndex: number): void {
this.animationInstance?.goToAndStop(frameIndex, true);
}
}
Rationale:
- Manual Lifecycle:
ngOnDestroy explicitly calls destroy() to prevent memory leaks, a critical step often missed.
- Type Safety: Uses
ReturnType for the animation instance to ensure correct method signatures.
- Extensibility: Public methods like
goToFrame expose the underlying API for parent component interaction.
4. Viewport-Aware Lazy Loading
Loading animations that are not immediately visible wastes bandwidth and CPU. Implement lazy loading using IntersectionObserver to defer initialization until the element enters the viewport.
import { Directive, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
import lottie from 'lottie-web';
@Directive({
selector: '[appLazyLottie]',
standalone: true
})
export class LazyLottieDirective implements AfterViewInit, OnDestroy {
private observer: IntersectionObserver | null = null;
private instance: any = null;
constructor(private elementRef: ElementRef) {}
ngAfterViewInit(): void {
this.observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
this.loadAnimation();
this.observer?.disconnect();
}
},
{ rootMargin: '200px' } // Preload slightly before visibility
);
this.observer.observe(this.elementRef.nativeElement);
}
private loadAnimation(): void {
const path = this.elementRef.nativeElement.getAttribute('data-lottie-path');
if (!path) return;
this.instance = lottie.loadAnimation({
container: this.elementRef.nativeElement,
renderer: 'svg',
loop: false,
autoplay: true,
path
});
}
ngOnDestroy(): void {
this.observer?.disconnect();
this.instance?.destroy();
}
}
Usage:
<div appLazyLottie data-lottie-path="assets/motion/hero.json" class="w-64 h-64"></div>
Rationale:
- Directive Pattern: Decouples lazy-loading logic from component implementation.
- Root Margin: Preloads animations 200px before visibility, ensuring smooth playback when the user scrolls.
- Data Attributes: Keeps configuration declarative in the template.
Pitfall Guide
| Pitfall | Explanation | Fix |
|---|
| Memory Leaks | Failing to call destroy() on the animation instance leaves DOM nodes and event listeners attached, causing gradual performance degradation in SPAs. | Always implement ngOnDestroy and invoke instance.destroy(). Use wrapper libraries that handle this automatically when possible. |
| Renderer Mismatch | Using canvas renderer unnecessarily increases CPU usage and reduces text crispness. SVG is preferred for most UI animations. | Default to renderer: 'svg'. Only switch to canvas if the animation contains thousands of vector paths causing SVG rendering bottlenecks. |
| Eager Loading Bloat | Importing large animation JSONs in the main bundle increases initial load time and TTFB. | Store animations in assets and load via HTTP request. Use lazy loading directives or dynamic imports for non-critical animations. |
| Initialization Race Conditions | Calling loadAnimation before the DOM element is ready results in errors or invisible animations. | Use AfterViewInit for direct API calls. Ensure ViewChild references are resolved. Wrapper components handle this internally. |
| Hardcoded Paths | Using absolute paths or paths that break with baseHref configuration causes 404 errors in deployed environments. | Use relative paths starting from the assets folder (e.g., assets/motion/file.json). Verify paths in angular.json assets configuration. |
| Ignoring Animation State | Assuming animations always play linearly; failing to handle pause, resume, or error states leads to broken UX. | Expose control methods (play, pause, stop) from wrapper components. Handle network errors when fetching JSON paths. |
| Bundle Size Neglect | Committing uncompressed JSON files with redundant layers or unused assets. | Compress animations using tools like IconKing before committing. Audit payload sizes in CI/CD pipelines. Target <50KB per animation. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Standard UI Feedback | ng-lottie Wrapper | Declarative, less boilerplate, automatic lifecycle management. | Low dev time, minimal maintenance. |
| Complex Frame Control | lottie-web Direct | Access to goToAndStop, setSpeed, and custom events. | Higher dev time, requires manual lifecycle care. |
| Below-the-Fold Content | Lazy Loading Directive | Defers network request and CPU usage until needed. | Improves initial load metrics. |
| High-DPI Requirements | SVG Renderer | Crisp rendering at any scale; vector quality. | Slightly higher memory vs canvas, but better UX. |
| Massive Animations (>200KB) | Dynamic Import / Lazy | Prevents blocking main thread; loads on demand. | Adds complexity; consider splitting animation. |
Configuration Template
Use this template to establish a consistent Lottie configuration across your Angular project. This includes a robust standalone component and asset setup.
app.config.ts (Optional Global Config):
import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimations()
// ng-lottie does not require global providers,
// but animations module may be needed for other features.
]
};
angular.json Assets Configuration:
{
"projects": {
"your-app": {
"architect": {
"build": {
"options": {
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "src/assets/motion",
"output": "/assets/motion"
}
]
}
}
}
}
}
}
Reusable LottiePlayerComponent Template:
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { LottieComponent, AnimationOptions } from 'ng-lottie';
@Component({
selector: 'app-lottie-player',
standalone: true,
imports: [LottieComponent],
template: `
<ng-lottie
[options]="config"
[width]="width"
[height]="height"
class="lottie-wrapper"
/>
`,
styles: [`
.lottie-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LottiePlayerComponent {
@Input({ required: true }) path!: string;
@Input() loop = false;
@Input() autoplay = true;
@Input() width = '100%';
@Input() height = '100%';
config: AnimationOptions;
constructor() {
this.config = {
path: this.path,
loop: this.loop,
autoplay: this.autoplay,
renderer: 'svg'
};
}
@Input('path')
set updatePath(newPath: string) {
this.path = newPath;
this.config.path = newPath;
}
}
Quick Start Guide
-
Install Dependencies:
npm install ng-lottie lottie-web
-
Add Animation Asset:
Place your compressed JSON file in src/assets/motion/feedback.json.
-
Create Player Component:
Generate a standalone component using the LottiePlayerComponent template above.
-
Integrate in Template:
<app-lottie-player
path="assets/motion/feedback.json"
[loop]="false"
width="200px"
height="200px"
/>
-
Verify:
Run ng serve and confirm the animation renders smoothly. Check network tab to ensure the JSON is fetched and payload size is optimized.