How to implement a global loader with Angular Material

How do you create a global loading spinner with Angular Material?


When it comes to implementing a loading spinner for your Angular application there are different ways to do this.

You could subscribe to the Angular router events.

You could also use an HTTP interceptor.

You can use a spinner inside your components - for example - or use a loading spinner on the Angular material buttons.

Or you could create a global spinner.

Whatever the case, here's the quickest way to add an Angular loading spinner to an Angular application.

Today, you're going to learn an easy way to set up a global spinning loader in Angular with Angular Material. This loading spinner that will be shown while your app is sending and receiving data from an API server.

1. Create a loading service

We'll be using the Angular CLI a lot so get your terminal (or command prompt) ready.

Here's the first command we'll use to generate our loading service. This service will be used as a mini state-manager for our application.

It would be better to use a state management library to manage the loading state of our application. But since I promised to make this fast, here's how to leap-frog the sticky work of getting a state manager set up.

ng generate service loading

Here's the code for our new service.

import { Injectable } from '@angular/core';
import { BehaviorSubject, delay, Observable } from 'rxjs';

  providedIn: 'root'
export class LoadingService {

  loading = new BehaviorSubject<boolean>(false);

  constructor() { }

  setLoading(loading: boolean) {;

2. Create the spinner component

This is the component that will create the overlay effect and display a loading spinner in the middle of the page.

ng generate component spinner

Here's the code for our component's Typescript file.

import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { LoadingService } from '../loading.service';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';

  selector: 'app-spinner',
  standalone: true,
  imports: [CommonModule, MatProgressSpinnerModule],
  templateUrl: './spinner.component.html',
  styleUrl: './spinner.component.scss'
export class SpinnerComponent {
  constructor(public loader: LoadingService) {}

As well as the code for our component's HTML file.

<div *ngIf="this.loader.loading | async" class="spinner-container">
    <div class="row">

And last of all, the CSS styles and animations for the loading spinner.

.spinner-container {
    height: 100%;
    padding: 0;
    margin: 0;
    display: flex;
    align-items: center;
    justify-content: center;
.row {
    width: auto;

3. Set up our global spinner component and API call

We will also need to add the new spinner component to the app.component.html file.

And also set up our simple API call.

Here's the Typescript code for our app.component.ts component.

import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { SpinnerComponent } from './spinner/spinner.component';
import { HttpClient } from '@angular/common/http';

  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, SpinnerComponent],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss'
export class AppComponent implements OnInit {
  data: any;
  constructor(private httpClient: HttpClient) { }

  ngOnInit(): void {
    .subscribe((response: any) => { = response.todos;

And the code for our app.component.html.

    @for (item of data; track $index) {
            <td>{{ data[$index].id }}</td>
            <td>{{ data[$index].todo }}</td>

4. Create an HTTP Interceptor

Our HTTP interceptor will be used to set the state of our loading spinner.

We'll begin by generating it with the Angular CLI.

ng generate interceptor app

Then, we'll modify our HTTP interceptor to spy on all outgoing requests and flip the state to loading. As soon as all outgoing requests have completed, it will flip the loading state back to false.

Here's the code.

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { LoadingService } from './loading.service';
import { finalize } from 'rxjs';

let totalRequests = 0;

export const appInterceptor: HttpInterceptorFn = (req, next) => {
  const loadingService = inject(LoadingService);
  return next(req).pipe(
    finalize(() => {
      if (totalRequests == 0) {

All that's left to do now is to register our HTTP interceptor inside the app.config.ts file.

We'll use the withInterceptors function like this.

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { appInterceptor } from './app.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [

We're done!

The Final Result 👏🏻 👏🏻

And here's a demo of the result.


What do you think of this global Material loading spinner? Is there any way it could be improved?

Or maybe you know of an easier or quicker way?

Don't hesitate to let me know what you think.


Angular Developer & Consultant

P.S. - Need an Angular expert to audit your code base? I've got a few slots open.