Material Feedback — Snackbar, Progress Indicators and Loading States

Consistent feedback UX — loading states, success confirmations, and error messages — requires a centralised notification system so every component does not independently manage its own toast/snackbar calls. A NotificationService wrapping MatSnackBar provides typed methods (success(), error(), info()) that any service or component can call with a single line. Combined with MatProgressBar for page-level loading and MatProgressSpinner for inline loading states, this gives the BlogApp production-quality feedback patterns.

NotificationService and Progress Indicators

import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
import { MatProgressBarModule }           from '@angular/material/progress-bar';
import { MatProgressSpinnerModule }       from '@angular/material/progress-spinner';

// ── Notification service — centralises all toast messages ─────────────────
@Injectable({ providedIn: 'root' })
export class NotificationService {
  private snackBar = inject(MatSnackBar);

  private show(message: string, panelClass: string, config?: MatSnackBarConfig): void {
    this.snackBar.open(message, 'Dismiss', {
      duration:    4000,
      horizontalPosition: 'end',
      verticalPosition:   'top',
      panelClass:  [panelClass],
      ...config,
    });
  }

  success(message: string): void { this.show(message, 'snack-success'); }
  error(message: string):   void { this.show(message, 'snack-error', { duration: 8000 }); }
  info(message: string):    void { this.show(message, 'snack-info'); }
  warn(message: string):    void { this.show(message, 'snack-warn'); }

  // With action button (returns reference for result Observable)
  confirm(message: string, action = 'Undo'): Observable<void> {
    const ref = this.snackBar.open(message, action, {
      duration: 5000,
      panelClass: ['snack-info'],
    });
    return ref.onAction();  // emits when user clicks the action button
  }
}

// ── Global styles for snackbar panels ──────────────────────────────────────
// styles.scss:
// .snack-success .mdc-snackbar__surface { background: var(--mat-sys-primary)   !important; }
// .snack-error   .mdc-snackbar__surface { background: var(--mat-sys-error)     !important; }
// .snack-warn    .mdc-snackbar__surface { background: var(--mat-sys-tertiary)  !important; }

// ── Page-level progress bar ────────────────────────────────────────────────
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, MatProgressBarModule],
  template: `
    <!-- Shows during route navigation ─────────────────────────────────── -->
    @if (navLoading.navigating()) {
      <mat-progress-bar mode="indeterminate" class="nav-progress" />
    }
    <app-shell>
      <router-outlet />
    </app-shell>
  `,
  styles: [`.nav-progress { position: fixed; top: 0; left: 0; right: 0; z-index: 1000; }`],
})
export class AppComponent {
  navLoading = inject(NavigationLoadingService);
}

// ── Button with spinner loading state ─────────────────────────────────────
@Component({
  standalone: true,
  imports: [MatButtonModule, MatProgressSpinnerModule],
  template: `
    <button mat-raised-button color="primary"
            [disabled]="isSaving()"
            (click)="onSave()">
      @if (isSaving()) {
        <mat-spinner diameter="18" class="btn-spinner" />
        Saving...
      } @else {
        <mat-icon>save</mat-icon>
        Save Post
      }
    </button>
  `,
  styles: [`.btn-spinner { display: inline-block; margin-right: 0.5rem; }`],
})
export class SaveButtonComponent {
  isSaving = signal(false);
  private notify = inject(NotificationService);

  onSave(): void {
    this.isSaving.set(true);
    this.api.savePost(this.form.getRawValue()).subscribe({
      next:  () => {
        this.isSaving.set(false);
        this.notify.success('Post saved successfully!');
      },
      error: () => {
        this.isSaving.set(false);
        this.notify.error('Failed to save post. Please try again.');
      },
    });
  }
}
Note: MatSnackBar.open() returns a MatSnackBarRef. Call ref.onAction() to get an Observable that emits when the user clicks the action button — perfect for undo functionality: show “Post deleted”, Undo button; if user clicks Undo within 5 seconds, call the restore API. ref.afterDismissed() emits when the snackbar closes (timeout or user dismiss). Use these for confirmation flows that need the snackbar result without a full dialog.
Tip: Define snackbar panel classes in styles.scss (global, not component-scoped) because Angular Material renders snackbars in an overlay portal outside the component’s DOM — component-scoped styles cannot reach them. Use the MDC surface class selector: .snack-success .mdc-snackbar__surface { background: green } for Angular Material 15+. The panelClass option adds CSS classes to the snackbar container, which you then style globally.
Warning: Do not show a snackbar notification for every HTTP response — it quickly becomes noise. Reserve snackbars for: user-triggered actions (saved, deleted, published), errors that require user attention, and undo opportunities. Do not show snackbars for: automatic background refreshes, pagination changes, or filter updates. The rule: if the user triggered an action and the action had a consequential result, show feedback. If the update is a consequence of another UI action they just took, the UI change itself is sufficient feedback.

Common Mistakes

Mistake 1 — Styling snackbar panels in component SCSS (styles don’t apply)

❌ Wrong — .snack-error { background: red } in component.scss; snackbar is in a portal outside component DOM; no effect.

✅ Correct — put snackbar panel styles in global styles.scss.

Mistake 2 — Showing snackbar notifications for every HTTP call (notification fatigue)

❌ Wrong — showing a “Loaded posts” snackbar every time the list loads; users dismiss them reflexively after the first few.

✅ Correct — reserve snackbars for user-triggered mutations (save, delete, publish) and errors, not routine data loads.

🧠 Test Yourself

A service calls notificationService.error('Save failed') after an API error. The snackbar has a panelClass of ‘snack-error’. The error styling is defined in the component’s SCSS file. The snackbar appears but with default styling. Why?