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
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.