Every admin dashboard needs toast notifications. Here's how I build them in Angular 21 with Bootstrap 5 — a service, a container component and individual toast components that work cleanly with Angular's change detection.

Toast Service

The service manages all toast state. I use signals here because they're the Angular 21 way:

// services/toast.service.ts
import { Injectable, signal } from '@angular/core'

export type ToastType = 'success' | 'error' | 'warning' | 'info'

export interface Toast {
  id: string
  message: string
  type: ToastType
  title?: string
  duration: number
}

@Injectable({ providedIn: 'root' })
export class ToastService {
  toasts = signal<Toast[]>([])

  show(message: string, type: ToastType = 'info', title?: string, duration = 4000) {
    const toast: Toast = {
      id: crypto.randomUUID(),
      message,
      type,
      title,
      duration,
    }
    this.toasts.update(current => [...current, toast])
  }

  success(message: string, title = 'Success') {
    this.show(message, 'success', title)
  }

  error(message: string, title = 'Error') {
    this.show(message, 'error', title, 6000)
  }

  warning(message: string, title = 'Warning') {
    this.show(message, 'warning', title)
  }

  info(message: string, title?: string) {
    this.show(message, 'info', title)
  }

  dismiss(id: string) {
    this.toasts.update(current => current.filter(t => t.id !== id))
  }
}

Toast Container Component

This goes in your root layout — renders all active toasts:

// components/toast-container/toast-container.component.ts
import { Component, inject } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ToastService, Toast } from '../../services/toast.service'
import { ToastItemComponent } from '../toast-item/toast-item.component'

@Component({
  selector: 'app-toast-container',
  standalone: true,
  imports: [CommonModule, ToastItemComponent],
  template: `
    <div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index:9999;">
      @for (toast of toastService.toasts(); track toast.id) {
        <app-toast-item [toast]="toast" (dismissed)="toastService.dismiss($event)" />
      }
    </div>
  `
})
export class ToastContainerComponent {
  toastService = inject(ToastService)
}

Individual Toast Component

// components/toast-item/toast-item.component.ts
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Toast } from '../../services/toast.service'

@Component({
  selector: 'app-toast-item',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="toast show mb-2 border-0 shadow-sm"
      role="alert"
      [class]="toastClass"
      style="min-width:280px;max-width:360px;">
      <div class="toast-header border-0" [class]="headerClass">
        <span class="me-2">{{ icon }}</span>
        <strong class="me-auto">{{ toast.title || defaultTitle }}</strong>
        <button type="button" class="btn-close btn-close-white opacity-75"
          (click)="onDismiss()"></button>
      </div>
      <div class="toast-body py-2">{{ toast.message }}</div>
    </div>
  `
})
export class ToastItemComponent implements OnInit, OnDestroy {
  @Input() toast!: Toast
  @Output() dismissed = new EventEmitter<string>()

  private timer?: ReturnType<typeof setTimeout>

  get toastClass(): string {
    return `text-white bg-${this.colorMap[this.toast.type]}`
  }

  get headerClass(): string {
    return `bg-${this.colorMap[this.toast.type]} text-white`
  }

  get icon(): string {
    return { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' }[this.toast.type]
  }

  get defaultTitle(): string {
    return { success: 'Success', error: 'Error', warning: 'Warning', info: 'Info' }[this.toast.type]
  }

  private colorMap: Record<string, string> = {
    success: 'success', error: 'danger', warning: 'warning', info: 'primary'
  }

  ngOnInit() {
    this.timer = setTimeout(() => this.onDismiss(), this.toast.duration)
  }

  ngOnDestroy() {
    if (this.timer) clearTimeout(this.timer)
  }

  onDismiss() {
    this.dismissed.emit(this.toast.id)
  }
}

Add Container to Layout

<!-- app-layout.component.html -->
<app-navbar />
<div class="d-flex">
  <app-sidebar />
  <main class="flex-grow-1 p-4">
    <router-outlet />
  </main>
</div>

<!-- Toasts render on top of everything -->
<app-toast-container />

Using the Service

From any component:

import { Component, inject } from '@angular/core'
import { ToastService } from '../../services/toast.service'

@Component({ ... })
export class UserFormComponent {
  private toast = inject(ToastService)

  async saveUser() {
    try {
      await this.userService.save(this.form.value)
      this.toast.success('User saved successfully')
    } catch (error) {
      this.toast.error('Failed to save user. Please try again.')
    }
  }

  deleteUser(id: string) {
    this.toast.warning('User deleted', 'Action completed')
  }
}

One-liner notifications from anywhere in the app. The signal-based service means the container re-renders automatically when new toasts are added or dismissed.

Frequently Asked Questions

Build your own Angular component rather than using Bootstrap's Toast JS directly. Angular's component architecture makes it cleaner — a ToastService pushes toasts to a signal-based array, and a ToastContainer component renders them. No Bootstrap JS needed, no change detection issues.
Inject the ToastService and call toast.show('Message', 'success'). The service holds state in a signal that the ToastContainer reads. Since the container is in the root layout, toasts appear regardless of which component triggers them.
In the ToastComponent's ngOnInit, start a setTimeout for the desired duration and call the dismiss method: setTimeout(() => this.dismiss(), this.duration). Clear the timeout on ngOnDestroy to prevent memory leaks.

Related

Need a Full Angular + Bootstrap Admin Dashboard?

Marvel Angular Dashboard — Angular 21 + Bootstrap 5 with 50+ components and dark mode.

Browse Templates →

Use code FIRST30 for 30% off.