A modern Angular toast notification library. Signals-first, zoneless-compatible, zero runtime dependencies.
For years, ngx-toastr was the standard choice for toast notifications in Angular. However, after it was compromised and subsequently abandoned, the Angular community was left without a modern, secure, and actively maintained alternative—especially one designed for modern Angular architectures (Signals-first, zoneless-compatible, and zero runtime dependencies).
ngx-herald was built to fill this gap: a clean, modern, secure toaster library built from the ground up for modern Angular applications.
- Signals-first — internal state is a single
signal<Toast[]>, OnPush throughout - Zoneless-compatible — no
zone.jsdependency, works withprovideExperimentalZonelessChangeDetection() - Zero runtime dependencies — no lodash, no tinycolor, nothing
- Both APIs — injectable
ToastServiceand imperativetoast.success(...)proxy - Promise / Observable support —
toast.promise()accepts both - Fully themeable — every visual decision is a CSS custom property
- Custom content — pass a
TemplateRefto render arbitrary Angular content inside a toast - Accessible —
role="log",role="alert"on errors, focus-visible dismiss button - Positions — six positions, per-toast or global
- Progress bar — pure CSS animation, zero JS overhead
- Angular 20+
- RxJS 7+ (peer dependency, only needed if you use
toast.promise()with an Observable)
npm install ngx-herald
# or
pnpm add ngx-heraldAdd the outlet component once at the root of your application (usually app.component.html):
<!-- app.component.html -->
<ngx-herald />
<router-outlet />That's it.
import { toast } from 'ngx-herald';
toast.success('File saved');
toast.error('Something went wrong', { description: 'Please try again.' });
toast.warning('Disk space low');
toast.info('Update available');import { ToastService } from 'ngx-herald';
@Component({ ... })
export class MyComponent {
private toasts = inject(ToastService);
save() {
this.toasts.success('Saved', { description: 'All changes persisted.' });
}
}Both APIs share the same signal state — they are interchangeable.
Every method accepts an optional ToastOptions object as second argument.
toast.success('Message', {
description: 'Optional subtitle text',
duration: 5000, // ms until auto-dismiss. 0 = never. Default: 4000
position: 'top-right', // override global position for this toast
progressBar: true, // default: true
dismissible: true, // show ✕ button. Default: true
template: myTemplateRef, // custom Angular template (see below)
});| Option | Type | Default | Description |
|---|---|---|---|
description |
string |
— | Secondary line below the message |
duration |
number |
4000 |
Auto-dismiss delay in ms. 0 disables it |
position |
ToastPosition |
global config | Overrides the position for this toast only |
progressBar |
boolean |
true |
Show the countdown bar |
dismissible |
boolean |
true |
Show the close button |
template |
TemplateRef |
— | Replaces the default content entirely |
const id = toast.success('File saved');
// dismiss one
toast.dismiss(id);
// dismiss all
toast.dismissAll();toast.promise() shows a loading toast immediately and transitions it to success or error when the async operation settles.
toast.promise(fetch('/api/save'), {
loading: 'Saving…',
success: 'Saved!',
error: 'Save failed.',
});The messages can be functions that receive the resolved value or the error:
toast.promise(uploadFile(file), {
loading: 'Uploading…',
success: (data) => `Uploaded ${data.filename}`,
error: (err) => `Upload failed: ${err.message}`,
});Both Promise<T> and Observable<T> are accepted. Observables are automatically completed after the first emission.
Pass a TemplateRef to take full control of the toast's content. The template receives the toast data and a dismiss function as context.
<ng-template #myToast let-t let-dismiss="dismiss">
<div style="padding: 12px 16px; display: flex; gap: 12px; align-items: center;">
<img [src]="t.options.description" alt="" width="40" height="40" style="border-radius: 50%;" />
<div>
<strong>{{ t.message }}</strong>
</div>
<button (click)="dismiss()" aria-label="Dismiss">✕</button>
</div>
</ng-template>private readonly myToast = viewChild<TemplateRef<unknown>>('myToast');
show() {
toast.success('Gabriel joined the team', {
template: this.myToast() as TemplateRef<never>,
description: 'https://example.com/avatar.jpg',
});
}interface ToastTemplateContext {
$implicit: Toast; // the full toast object
dismiss: () => void;
}To configure global defaults for every toast, register provideToaster() in your application providers (usually in app.config.ts). All fields are optional:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideToaster } from 'ngx-herald';
export const appConfig: ApplicationConfig = {
providers: [
provideToaster({
position: 'bottom-right', // default: 'bottom-right'
duration: 4000, // default: 4000
progressBar: true, // default: true
dismissible: true, // default: true
maxToasts: 5, // default: 5 — oldest is dropped when exceeded
}),
],
};'top-left' | 'top-center' | 'top-right'
'bottom-left' | 'bottom-center' | 'bottom-right'
If you need toasts at a specific position regardless of individual toast settings, pass position directly on <ngx-herald>:
<ngx-herald position="top-right" />ngx-herald ships with neutral defaults that inherit your app's font. Every visual decision is a CSS custom property — override what you need in your global stylesheet.
:root {
/* layout */
--ngx-toast-z-index: 9999;
--ngx-toast-gap: 8px;
--ngx-toast-width: 360px;
--ngx-toast-padding: 12px 16px;
--ngx-toast-border-radius: 8px;
--ngx-toast-offset: 16px;
/* typography */
--ngx-toast-font-family: inherit;
--ngx-toast-font-size: 14px;
--ngx-toast-message-weight: 500;
--ngx-toast-message-color: #111827;
--ngx-toast-description-color: #6b7280;
--ngx-toast-description-size: 13px;
/* visual */
--ngx-toast-bg: #ffffff;
--ngx-toast-border: 1px solid rgba(0, 0, 0, 0.08);
--ngx-toast-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
/* type accent colors */
--ngx-toast-success-color: #22c55e;
--ngx-toast-error-color: #ef4444;
--ngx-toast-warning-color: #f59e0b;
--ngx-toast-info-color: #3b82f6;
/* progress bar */
--ngx-toast-progress-height: 3px;
--ngx-toast-progress-opacity: 0.3;
}@media (prefers-color-scheme: dark) {
:root {
--ngx-toast-bg: #1f2937;
--ngx-toast-border: 1px solid rgba(255, 255, 255, 0.08);
--ngx-toast-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
--ngx-toast-message-color: #f9fafb;
--ngx-toast-description-color: #9ca3af;
}
}:root {
--ngx-toast-font-family: var(--mat-sys-body-medium-font);
--ngx-toast-border-radius: 4px;
--ngx-toast-bg: var(--mat-sys-surface);
--ngx-toast-message-color: var(--mat-sys-on-surface);
--ngx-toast-border: none;
--ngx-toast-shadow: var(--mat-sys-level2);
}| ngx-herald | ngx-toastr | Angular Material Snackbar | |
|---|---|---|---|
| Angular 20+ / zoneless | ✅ | ❌ abandoned | ✅ |
| Signals-first | ✅ | ❌ | ❌ |
| Zero dependencies | ✅ | ❌ | ❌ |
| Imperative API | ✅ | ✅ | ❌ |
| Injectable service | ✅ | ✅ | ✅ |
| Promise / Observable | ✅ | ❌ | ❌ |
| Custom TemplateRef | ✅ | partial | ✅ |
| CSS custom properties | ✅ | ❌ | partial |
| npm provenance | ✅ | ❌ | — |
Pull requests are welcome. For significant changes please open an issue first.
git clone https://github.com/HoplaGeiss/ngx-herald.git
cd ngx-herald
pnpm install
ng serve demo # starts the demo app at localhost:4200