cms

Tag: Angular

  • CSP Compliance in Angular + FastAPI: Removing ‘unsafe-inline’ the Right Way

    CSP Compliance in Angular + FastAPI: Removing ‘unsafe-inline’ the Right Way

    Error

    Introduction

    During a recent security review, our team flagged a critical issue in our Content Security Policy (CSP): the use of 'unsafe-inline'.

    A CSP is an HTTP header that tells the browser which resources (scripts, styles, images, etc.) are allowed to load. It is one of the strongest defenses against Cross-Site Scripting (XSS) and other injection attacks. However, the directive 'unsafe-inline' weakens this protection by allowing any inline JavaScript or CSS to run. While convenient during development, it poses a serious vulnerability in production and is commonly flagged by security teams.

    When we removed 'unsafe-inline', the application immediately broke — Angular scripts failed, inline styles were blocked, and third-party libraries like Font Awesome and Flatpickr stopped working.

    Our setup is a standalone Angular frontend and a FastAPI backend, both deployed in the same App Service. The backend serves the Angular app by routing all wildcard paths (/*) to index.html. With both UI and API sharing the same deployment, CSP changes had to be applied carefully to avoid breaking either layer.

    This post explains how we solved these issues step by step to achieve full CSP compliance while keeping the application fully functional.

    Problem Statement

    Removing 'unsafe-inline' caused multiple issues:

    • Angular scripts and styles failed – Angular depends on certain inline initializations.
    • Third-party libraries broke – Font Awesome and Flatpickr injected CSS blocked by CSP.
    • Inline styles were blocked – Angular-generated styles needed explicit allowance.
    • Build conflicts – Angular’s default CSS optimizations didn’t play well with a strict CSP.

    Keeping 'unsafe-inline' was not an option. The challenge was to make our app secure without losing functionality, and to ensure the fix was maintainable long-term.

    The Original CSP Policy

    Here’s what our CSP looked like before the fix (with client-specific domains/CDNs removed for clarity):

    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
        "connect-src 'self'; "
        "font-src 'self' https://fonts.gstatic.com; "
        "img-src 'self' https://cdn.jsdelivr.net; "
        "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
        "frame-ancestors 'self'; "
        "frame-src 'self'; "
        "form-action 'self';"
    )

    This worked in development but had serious drawbacks:

    • It depended on 'unsafe-inline', which defeats CSP’s core protections.
    • It wasn’t strict enough for production security.
    • Our security team marked it as a critical vulnerability.

    This was the baseline we had to improve.

    Why This Policy Was Insecure

    On paper, the CSP looked restrictive: most sources were locked to 'self' with a few exceptions (Google Fonts, jsDelivr). But 'unsafe-inline' completely undermined it.

    • Inline scripts or styles could run unchecked, leaving the app open to XSS.
    • No nonces or hashes were used to whitelist trusted inline code.
    • The CSP gave a false sense of security: restrictive in appearance, permissive in practice.

    This is why we had to redesign the policy to remove 'unsafe-inline' while keeping the Angular + FastAPI app functional.

    Solution

    Fixing our CSP wasn’t just about removing 'unsafe-inline'. We had to carefully update both the Angular build and third-party integrations so the app would still run under a strict policy. Here’s what we did:

    1. Removed 'unsafe-inline'

    The first step was to remove 'unsafe-inline' from the CSP header. This was necessary for security, but it immediately broke the app:

    • Angular scripts and styles stopped working.
    • Third-party libraries like Font Awesome and Angular Material failed to load inline CSS or initialization scripts.
    • Multiple console errors appeared, e.g.:
    Refused to execute inline event handler because it violates the following Content Security Policy directive: 
    "script-src 'self' ". 
    Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution. 
    Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present.

    This confirmed that simply removing 'unsafe-inline' broke functionality, so we needed a way to allow trusted inline code securely.

    2. Introduced Nonces

    To replace 'unsafe-inline' safely, we added nonces. A nonce is a random value generated per request, included in the CSP header, and injected into inline <script> and <style> tags. Only code with the matching nonce is executed by the browser.

    import secrets
    
    # Generate a random nonce for each response
    nonce = secrets.token_urlsafe(16)

    The corresponding CSP header now uses the nonce instead of 'unsafe-inline':

    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; "
        "connect-src 'self'; "
        "font-src 'self' https://fonts.gstatic.com; "
        "img-src 'self' https://cdn.jsdelivr.net; "
        f"style-src 'self' 'nonce-{nonce}' https://fonts.googleapis.com; "
        "frame-ancestors 'self'; "
        "frame-src 'self'; "
        "form-action 'self';"
    )

    In our setup, the UI’s index.html is served by the FastAPI backend via a wildcard route (/*), so we inject the nonce dynamically when returning the page. We also attach it to the <app-root> element for Angular:

    # The same nonce needs to be attached.
    # Store the value in request object / global etc to pass it around.
    # Attach ngCspNonce to <app-root>
    content = re.sub(
        r'<app-root(?![^>]*\bngCspNonce=)([^>]*)>',
        fr'<app-root\1 ngCspNonce="{nonce}">',
        content,
    )
    
    # Attach nonce to all inline <script> and <style> tags
    content = re.sub(
        r'(<script(?![^>]*\bnonce=))',
        fr'\1 nonce="{nonce}"',
        content
    )
    content = re.sub(
        r'(<style(?![^>]*\bnonce=))',
        fr'\1 nonce="{nonce}"',
        content
    )
    

    Key points:

    Using the wildcard route for index.html allows this injection to happen on every response, ensuring the frontend always receives a valid nonce.

    'unsafe-inline' is fully removed.

    Nonces are generated per request and injected dynamically into <app-root>, inline <script>, and <style> tags.

    Angular, Angular Material, and other libraries can safely run inline code without breaking CSP.

    3. Updated Angular Build Configuration

    After introducing nonces in Step 2, most CSP-related errors disappeared, reducing console violations from around 15 to just 2. However, a couple of issues remained:

    1. Critical CSS inlining in Angular’s default build was still causing CSP violations.
    2. Font Awesome, which we use for icons, automatically injects CSS at runtime. This violates CSP because the styles are inline.

    To resolve these issues, we made the following updates:

    a) Adjust Angular Build

    We updated angular.json (indside the build object) to disable inlined critical CSS while keeping other optimizations:

    "optimization": {
      "scripts": true,
      "styles": {
        "minify": true,
        "inlineCritical": false
      },
      "fonts": true
    }

    This change:

    • Prevents inline critical CSS from violating CSP.
    • Ensures all styles are bundled as external files compatible with nonces or trusted sources.
    • Maintains minification and font optimizations.

    b) Disable Font Awesome Auto CSS Injection

    Font Awesome injects CSS at runtime, which breaks a strict CSP. Since we use Font Awesome for icons, we disabled this behavior in app.config.ts:

    import { config } from '@fortawesome/fontawesome-svg-core';
    config.autoAddCss = false;

    With this, Font Awesome CSS must be loaded manually via trusted external files, ensuring compliance with CSP.

    After these changes, all remaining CSP errors were resolved except for a small inline CSS from Flatpickr, which we handled in the next step.

    4. Handling Flatpickr CSS with a Style Hash

    After updating the Angular build and fixing Font Awesome, all CSP errors were resolved except for one: inline styles injected by Flatpickr. These styles cannot use nonces because they are generated at runtime.

    The browser console helped here by providing the exact SHA-256 hash of the blocked CSS in the CSP violation message. We then added this hash to the style-src directive of the CSP header to allow only this specific inline style to execute:

    # Example SHA-256 hash for Flatpickr CSS
    flatpickr_hash = "'sha256-t4I2teZN5ZH+VM+XOiWlaPbsjQHe+k9d6viXPpKpNWA='"
    
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        f"script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; "
        "connect-src 'self'; "
        "font-src 'self' https://fonts.gstatic.com; "
        "img-src 'self' https://cdn.jsdelivr.net; "
        f"style-src 'self' 'nonce-{nonce}' https://fonts.googleapis.com {flatpickr_hash}; "
        "frame-ancestors 'self'; "
        "frame-src 'self'; "
        "form-action 'self';"
    )

    Key points:

    • The browser error provided the exact hash, allowing us to whitelist only that CSS.
    • This approach keeps the CSP strict and secure, without reintroducing 'unsafe-inline'.
    • After this final step, all scripts and styles — including Angular, Angular Material, Font Awesome, and Flatpickr — worked correctly under a strict CSP.

    This completes the migration from a permissive, unsafe CSP to a secure, nonce- and hash-based policy without breaking application functionality.

    Conclusion

    Migrating from a permissive CSP with 'unsafe-inline' to a strict, secure policy required careful adjustments across both the Angular frontend and FastAPI backend. By removing 'unsafe-inline', introducing nonces, updating the Angular build configuration, handling Font Awesome CSS, and whitelisting runtime-generated Flatpickr styles with a hash, we achieved full CSP compliance without breaking the application.

    Key takeaways:

    • Nonces allow trusted inline scripts and styles to run while blocking everything else.
    • Build configuration matters — Angular’s default critical CSS inlining can conflict with CSP.
    • Third-party libraries like Font Awesome and Flatpickr often require special handling.
    • Browser-provided hashes can safely whitelist specific inline CSS when nonces aren’t possible.

    With this approach, the application is now secure against Cross-Site Scripting (XSS) attacks, fully functional, and production-ready. This workflow provides a practical blueprint for developers aiming to enforce strict CSPs in Angular + FastAPI applications.

  • Preventing XSS in Angular: Building a Custom Malicious Content Validator

    Preventing XSS in Angular: Building a Custom Malicious Content Validator

    XSS

    Introduction

    User input is one of the most common gateways for security vulnerabilities, especially Cross-Site Scripting (XSS) attacks. If malicious code slips into your application, it can execute in your users’ browsers, leading to stolen data, compromised accounts, and damaged trust. While Angular provides some built-in protection against XSS, you should still validate inputs at the form level to catch and block unsafe content early.

    In this article, we’ll walk through creating a custom Angular validator that uses regex patterns to detect and reject malicious inputs such as embedded scripts, encoded HTML tags, and event handlers. By integrating this validator into your forms, you can add an extra layer of defense to your application’s security.

    Understanding the Malicious Patterns

    Our validator works by scanning user input against a set of regular expressions — each designed to catch a different category of potentially dangerous content. Let’s break them down.

    Script and media tags

    /<script/,
    /<img/,
    /<(iframe|object|embed|svg|link|style)/,

    These patterns look for HTML tags that are notorious for carrying malicious payloads:

    • <script> — The classic XSS injection point. If an attacker can insert this, they can run arbitrary JavaScript in the user’s browser.
    • <img> — Often used with onerror to trigger JavaScript when the image fails to load.
    • <iframe>, <object>, <embed>, <svg>, <link>, <style> — These tags can load external resources, execute scripts, or inject styles that manipulate the page in unsafe ways.

    Event handler attributes

    /on\w+=/, // e.g., onload=, onclick=

    Any HTML attribute starting with on can execute JavaScript when a certain event occurs.


    For example:

    <div onmouseover="alert('XSS')"></div>

    This regex catches all of them — onload, onclick, onmouseover, and many more.

    JavaScript protocols and data URIs

    /javascript:/,
    /data:(text\/html|text\/javascript|application\/javascript);base64/,

    • javascript: — A URL scheme that executes JavaScript code directly in the browser. Common in malicious <a href> links.
    • data: URIs — These can embed Base64-encoded HTML or JavaScript directly inside an element source. For example:
    <img src="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">

    Both allow attackers to smuggle in executable code without needing an external file.

    Encoded characters

    /%3c/, // encoded <
    /%3e/, // encoded >
    /&#x[0-9a-f]+;/,
    /\\x[0-9a-f]{2}/,
    /\\u[0-9a-f]{4}/,

    Attackers often try to obfuscate dangerous characters so they slip past filters:

    • %3c → URL-encoded <
    • %3e → URL-encoded >
    • &#x...; → HTML entity encoding (hexadecimal)
    • \x.. → Hexadecimal escape sequences in JavaScript strings
    • \u.... → Unicode escape sequences

    Even if your HTML filter catches <script>, these encodings could bypass it — unless you detect them too. By grouping our patterns this way, we cover both obvious attack vectors (like <script>) and sneaky obfuscation tricks that more sophisticated attackers might use.

    Implementing the XSS-Blocking Validator

    To protect your Angular forms from Cross-Site Scripting (XSS) attacks, we can create a custom validator that scans user input for suspicious patterns before it’s ever sent to the server.
    The core idea is:

    1. Normalize the input so attackers can’t sneak around with spaces or case variations.
    2. Match against a set of known dangerous patterns — things like <script> tags, event handlers (onload=, onclick=), JavaScript URIs, and encoded HTML characters.
    3. Return an error if any match is found so the form can block submission.

    Here’s how that looks in code:

    import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
    
    export class CustomValidators {
      static maliciousContentValidator(): ValidatorFn {
        return (control: AbstractControl): ValidationErrors | null => {
          if (!control.value) return null; // Allow empty values
    
          // Step 1: Normalize input
          const value = String(control.value).toLowerCase().replace(/\s+/g, '');
    
          // Step 2: Patterns for malicious content
          const patterns = [
            /<script/,
            /<img/,
            /<(iframe|object|embed|svg|link|style)/,
            /on\w+=/, // event handlers like onload=, onclick=
            /javascript:/,
            /data:(text\/html|text\/javascript|application\/javascript);base64/,
            /%3c/, // encoded <
            /%3e/, // encoded >
            /&#x[0-9a-f]+;/,
            /\\x[0-9a-f]{2}/,
            /\\u[0-9a-f]{4}/,
          ];
    
          // Step 3: Check for matches
          const isMalicious = patterns.some((pattern) => pattern.test(value));
    
          // Step 4: Return validation result
          return isMalicious ? { maliciousContent: true } : null;
        };
      }
    }
    

    Add it to a form control

    Once the validator is defined, you can plug it into any Angular FormControl alongside built-in validators. This way, your field will automatically block unsafe input during normal form validation.

    this.form = this.fb.group({
      title: [
        '',
        [
          Validators.required,
          Validators.maxLength(40),
          CustomValidators.maliciousContentValidator(),
        ],
      ],
    });

    Using it in the HTML

    After attaching the validator to your form control, you can show a helpful error message to the user when their input contains suspicious content. We check the control’s errors object and only display the message after the field has been touched to avoid flashing errors too early.

    <input
      formControlName="title"
      placeholder="Enter title"
    />
    
    @if (form.get('title')?.hasError('maliciousContent') && form.get('title')?.touched) {
      <div class="error">
        Your input looks unsafe (possible XSS). Please remove script-like content.
      </div>
    }
    

    This ensures that users get immediate feedback if they accidentally (or intentionally) enter something that resembles an XSS payload.

    Conclusion

    While Angular already includes built-in XSS protections, validating user input at the form level adds another important layer of defense. By implementing a custom maliciousContentValidator, you can catch and block potentially dangerous patterns before they ever reach your server. This approach not only strengthens security against XSS attacks but also improves the user experience by providing immediate, clear feedback when unsafe content is detected.

    Remember — client-side validation should always be paired with robust server-side checks and proper output encoding. Security works best when it’s defense in depth.

  • Building a Generic Loading Service & HTTP Interceptor in an Angular Standalone

    Building a Generic Loading Service & HTTP Interceptor in an Angular Standalone

    Loading generic component

    Problem & Goal

    In many Angular applications, developers manually control loading indicators inside each component. While this works for small projects, it quickly becomes repetitive, error-prone, and hard to maintain as the codebase grows.

    A better approach is to centralize loading state management. By using a generic loading service combined with an HTTP interceptor, we can automatically track when requests start and finish — showing a global progress spinner without writing extra code in every component.

    We’ll also add a configurable flag to skip the progress spinner for certain requests — useful for silent background calls or analytics pings that don’t need to interrupt the user experience.

    Our goals are:

    1. Centralized Loading State – Manage loading indicators in one place instead of scattering logic across components.
    2. Automatic Triggering – Show/hide a spinner automatically for all HTTP requests.
    3. Configurable Behavior – Allow opting out of the spinner per-request using a flag.
    4. Standalone-Friendly – Ensure the solution works seamlessly in Angular standalone projects without relying on NgModules.

    Creating the Loader Component

    Before we set up our HTTP interceptor, we first need a Loader Component that can display a progress spinner overlay whenever our application is performing network requests. This will serve as the visual feedback for the user, showing that something is happening in the background.

    We’ll use Angular Material’s <mat-progress-spinner> because it’s lightweight, accessible, and easy to style.

    Loader Template

    @if (loadingService.isLoading$ | async) {
      <div class="loader-overlay">
        <mat-progress-spinner
          class="loading-spinner"
          mode="indeterminate"
          diameter="64"
        ></mat-progress-spinner>
      </div>
    }

    How it works:

    • We’re using Angular’s new template syntax @if to conditionally display the loader only when the isLoading$ observable emits true.
    • The mat-progress-spinner is set to indeterminate mode so it spins continuously until the request completes.
    • Wrapping it in a .loader-overlay div ensures it’s centered and blocks user interaction while active.

    Loader Component Class

    import { Component, inject } from '@angular/core';
    import { LoadingService } from '../services/loading.service';
    
    @Component({
      selector: 'app-loader',
      standalone: true,
      templateUrl: './loader.component.html',
      styleUrls: ['./loader.component.scss'],
    })
    export class LoaderComponent {
      loadingService = inject(LoadingService);
    }

    The component is kept intentionally simple — it just injects the LoadingService and binds to its isLoading$ observable.

    Loader Styles

    .loader-overlay {
      position: fixed;
      width: 100%;
      height: 100%;
      left: 0;
      top: 0;
      background-color: rgba(6, 27, 44, 0.2);
      z-index: 9999;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
    }
    
    .loading-spinner {
      height: 64px;
    }

    This styling ensures the loader:

    • Covers the entire screen.
    • Has a subtle dark overlay to focus user attention.
    • Centers the spinner both vertically and horizontally.

    With the loader component ready, the next step is to create a Loading Service that will control when this spinner is shown — and later, we’ll hook it into an HTTP interceptor so it all works automatically.

    Creating the Loading Service

    Now that we have our loader component, we need a way to control when it appears. The LoadingService will be our central state manager for tracking ongoing HTTP requests.

    Instead of just toggling a boolean, this service uses a counter (apiCount) to handle multiple simultaneous API calls. That way, the loader won’t disappear until all requests have finished.

    Loading Service Code

    import { Injectable } from '@angular/core';
    import { BehaviorSubject } from 'rxjs';
    
    @Injectable({
      providedIn: 'root',
    })
    export class LoadingService {
      // Tracks the number of ongoing API calls
      private apiCount = 0;
    
      private isLoadingSubject = new BehaviorSubject<boolean>(false);
      isLoading$ = this.isLoadingSubject.asObservable();
    
      /** Show loader (when an API starts) */
      showLoader() {
        if (this.apiCount === 0) {
          this.isLoadingSubject.next(true);
        }
        this.apiCount++;
      }
    
      /** Hide loader (when an API completes) */
      hideLoader() {
        if (this.apiCount > 0) {
          this.apiCount--;
        }
        if (this.apiCount === 0) {
          this.isLoadingSubject.next(false);
        }
      }
    
      /** Force hide the loader (e.g., when polling stops) */
      forceHideLoader() {
        this.apiCount = 0;
        this.isLoadingSubject.next(false);
      }
    }

    How it works

    • apiCount keeps track of the number of ongoing API calls.
    • isLoadingSubject emits true when the first API starts and false only when the last one completes.
    • showLoader() increments the counter and turns on the spinner if it’s the first request.
    • hideLoader() decrements the counter and hides the spinner when all requests are done.
    • forceHideLoader() immediately resets the counter and hides the loader — handy for scenarios like stopping background polling.

    With the loader component and loading service ready, the next step is to create an HTTP interceptor that will automatically call showLoader() and hideLoader() for us — and respect a configurable flag to skip the loader for certain requests.

    Creating the HTTP Interceptor

    Manually calling showLoader() and hideLoader() in every component would defeat the purpose of our centralized solution. Instead, we’ll use an HTTP interceptor to hook into every request and response automatically.

    This interceptor will:

    1. Call showLoader() when a request starts.
    2. Call hideLoader() when the request finishes (success or error).
    3. Respect a skip flag (X-Skip-Loader header) so certain requests won’t trigger the loader.

    Interceptor Code

    import { HttpInterceptorFn } from '@angular/common/http';
    import { inject } from '@angular/core';
    import { LoadingService } from '../../services/loading/loading.service';
    import { finalize } from 'rxjs';
    
    export const loaderInterceptor: HttpInterceptorFn = (req, next) => {
      const loadingService = inject(LoadingService);
    
      // If this request has the custom header, skip showing the loader
      const shouldSkipLoader = req.headers.has('X-Skip-Loader');
    
      if (!shouldSkipLoader) {
        loadingService.showLoader();
      }
    
      return next(req).pipe(
        finalize(() => {
          if (!shouldSkipLoader) {
            loadingService.hideLoader();
          }
        })
      );
    };

    How it works

    • Skip flag: By adding a custom header X-Skip-Loader to any HTTP request, we tell the interceptor not to show the spinner for that request. This is useful for silent background calls like analytics pings or cache warmups.
    • Automatic tracking: The interceptor calls showLoader() before passing the request forward, and uses finalize() to guarantee hideLoader() runs when the request completes, fails, or is canceled.
    • Standalone-friendly: This uses Angular’s HttpInterceptorFn functional API, which works seamlessly in standalone projects without the need for traditional @Injectable() class-based interceptors.

    Example — Skipping the Loader for a Request

    this.http.get('/api/analytics', {
      headers: { 'X-Skip-Loader': '' }
    }).subscribe();

    With the loader component, loading service, and interceptor in place, you now have a fully automated, configurable global loading indicator in your Angular standalone project.

    In the next part, we’ll bring everything together by registering the interceptor and adding the loader component to the app so it works across all pages.

    Putting It All Together

    We now have:

    • A Loader Component that displays a progress spinner overlay.
    • A Loading Service that tracks ongoing HTTP requests.
    • An HTTP Interceptor that automatically triggers the loader and supports skipping it with a custom header.

    Let’s integrate these pieces into our Angular standalone app.

    Register the Interceptor

    In a standalone Angular project, interceptors are added in the providers array of bootstrapApplication.

    import { bootstrapApplication } from '@angular/platform-browser';
    import { provideHttpClient, withInterceptors } from '@angular/common/http';
    import { AppComponent } from './app/app.component';
    import { loaderInterceptor } from './app/interceptors/loader/loader.interceptor';
    
    bootstrapApplication(AppComponent, {
      providers: [
        provideHttpClient(withInterceptors([loaderInterceptor])),
      ],
    }).catch((err) => console.error(err));

    This ensures every HTTP request in your app passes through the loaderInterceptor.

    Add the Loader Component to the Root Template

    Place the loader component inside your AppComponent template so it can be displayed globally:

    <app-loader></app-loader>
    <router-outlet></router-outlet>

    Since the LoaderComponent listens to LoadingService.isLoading$, it will automatically appear and disappear without any additional wiring in individual components.

    Skipping the Loader for Certain Requests

    For requests where you don’t want to block the UI with a spinner, simply include the X-Skip-Loader header:

    this.http.get('/api/analytics', {
      headers: { 'X-Skip-Loader': '' }
    }).subscribe();

    This keeps the UI uninterrupted for silent background operations.

    Final Thoughts

    With just a few pieces — a loader component, a loading service, and a functional HTTP interceptor — we’ve created a centralized, configurable global loading indicator for an Angular standalone project.

    The benefits are clear:

    • No more repetitive spinner logic in multiple components.
    • Consistent user experience for all network calls.
    • Fine-grained control to skip the loader when needed.

    This pattern is clean, scalable, and fully compatible with Angular’s modern standalone architecture.

  • Using Lottie Icons in Angular Standalone Projects

    Using Lottie Icons in Angular Standalone Projects

    Introduction

    Animations can add a lot of life to a web app — but let’s be honest, Animations can bring a web app to life — whether it’s a subtle loading spinner or a bold hero icon, they make your UI feel smoother and more modern. But traditional formats like GIFs or sprite sheets? Not so great. They’re heavy, not scalable, and kind of stuck in the past.

    That’s where Lottie comes in. Lottie lets you render beautiful, high-quality animations using small JSON files. They’re lightweight, resolution-independent, and easy to control — a perfect fit for today’s fast, interactive frontends.

    In this post, I’ll walk you through how to set up and use Lottie animations in Angular, using a clean and modern approach. It’s quick to get started, and once you do, you’ll probably never want to go back to static icons again.

    Here’s what you’ll learn:

    • What Lottie animations are and why they’re awesome for modern UIs
    • How to set up Lottie in your Angular standalone project
    • How to add and reuse animations in your components

    Let’s jump in and make your app a little more fun ✨

    What Lottie animations are and why they’re awesome for modern UIs

    Lottie is an open-source animation library developed by Airbnb that lets you render animations created in Adobe After Effects (exported via Bodymovin) as real-time, vector-based animations in your app — using just JSON.

    Unlike GIFs or video files, Lottie animations are:

    • Lightweight – they load quickly and don’t bloat your bundle size.
    • Scalable – no quality loss on high-DPI screens.
    • Interactive – you can control playback, respond to user actions, or even sync them with state changes.

    They’re perfect for:

    • Loading spinners
    • Button feedback
    • Empty states and onboarding screens
    • Decorative illustrations

    Most developers use LottieFiles — a massive library of free (and premium) animations you can browse, customize, and export directly as JSON files for use in your app.

    So instead of using clunky image assets or reinventing animation from scratch, Lottie gives you plug-and-play motion design — and your users will feel the difference.

    How to set up Lottie in your Angular standalone project

    Once you’ve got your Angular project set up, adding Lottie is pretty straightforward. We’ll be using the official Angular wrapper for Lottie: ngx-lottie.

    Install the necessary packages:

    npm install lottie-web ngx-lottie

    Next, configure the Lottie player globally using provideLottieOptions. This can go in your app.config.ts:

    import {
      ApplicationConfig,
      provideBrowserGlobalErrorListeners,
      provideZoneChangeDetection,
    } from '@angular/core';
    import { provideRouter } from '@angular/router';
    import { provideLottieOptions } from 'ngx-lottie';
    import player from 'lottie-web';
    
    import { routes } from './app.routes';
    
    export const appConfig: ApplicationConfig = {
      providers: [
        provideBrowserGlobalErrorListeners(),
        provideZoneChangeDetection({ eventCoalescing: true }),
        provideRouter(routes),
        provideLottieOptions({
          player: () => player,
        }),
      ],
    };

    And that’s it — Lottie is now wired up and ready to use in your components!

    How to Add and Reuse Animations in Your Components

    Now that Lottie is wired up, let’s make it easy to drop animations anywhere in your app.

    Instead of repeating the same Lottie config in every component, we’ll create a dedicated LottieIconComponent. This keeps things clean and lets you reuse animations by just passing in a few inputs like the path, size, and autoplay settings.

    Creating the LottieIconComponent

    To get started, create a new standalone component:

    ng generate component lottie-icon

    Set up the component using signal inputs

    Let’s wire it up to accept a path to the animation, along with optional size and behavior flags.

    // lottie-icon.ts
    import { Component, computed, input } from '@angular/core';
    import { AnimationOptions, LottieComponent } from 'ngx-lottie';
    
    @Component({
      selector: 'app-lottie-icon',
      imports: [LottieComponent],
      templateUrl: './lottie-icon.html',
      styleUrl: './lottie-icon.css',
    })
    export class LottieIcon {
      path = input<string>('');
      width = input<string>('100px');
      height = input<string>('100px');
      loop = input<boolean>(true);
      autoplay = input<boolean>(true);
    
      readonly options = computed<AnimationOptions>(() => ({
        path: this.path(),
        loop: this.loop(),
        autoplay: this.autoplay(),
      }));
    }

    <!-- lottie-icon.html -->
    <ng-lottie
      [options]="options()"
      [styles]="{ width: width(), height: height() }"
    ></ng-lottie>

    Use It Anywhere in Your App

    Before using your Lottie icon, make sure your JSON animation file is placed where Angular can serve it statically.

    📁 Put Your Icon in the Public Folder

    Drop your animation file into the public/icons directory. For example:

    public/icons/lottie-icon.json

    Files in the public/ folder are served as-is at the root of your app, so the URL to this file will be /icons/lottie-icon.json.

    You can now drop your Lottie icons into any component where you want a bit of motion or visual feedback. They’re perfect for things like loading states, empty screens, confirmation messages, or just to add a little personality to your UI.

    Here’s how you can use the reusable component:

    <app-lottie-icon 
      [path]="'/icons/lottie-icon.json'" 
      [width]="'150px'" 
      [height]="'150px'" 
      [loop]="true" 
      [autoplay]="true">
    </app-lottie-icon>

    This example loads an animation from your /public/icons/ folder, sizes it to 150×150 pixels, and sets it to autoplay in a loop. You can easily switch out the path to show different icons, and adjust the width, height, loop, or autoplay settings as needed.

    🧵 In Summary

    Lottie makes it super easy to add sleek, scalable animations to your Angular app — and with a reusable component setup, you don’t have to repeat yourself. Whether you’re adding a fun touch to a button or showing a loading state with flair, Lottie helps your UI feel more modern and alive.

    Now you’ve got the setup, the component, and the flexibility — go ahead and bring your UI to life!