ngx-herald

June 5, 2026 · View on GitHub

A modern Angular toast notification library. Signals-first, zoneless-compatible, zero runtime dependencies.

CI npm license

Live demo →

Why ngx-herald?

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.


Features

  • Signals-first — internal state is a single signal<Toast[]>, OnPush throughout
  • Zoneless-compatible — no zone.js dependency, works with provideExperimentalZonelessChangeDetection()
  • Zero runtime dependencies — no lodash, no tinycolor, nothing
  • Both APIs — injectable ToastService and imperative toast.success(...) proxy
  • Promise / Observable supporttoast.promise() accepts both
  • Fully themeable — every visual decision is a CSS custom property
  • Custom content — pass a TemplateRef to render arbitrary Angular content inside a toast
  • Accessiblerole="log", role="alert" on errors, focus-visible dismiss button
  • Positions — six positions, per-toast or global
  • Progress bar — pure CSS animation, zero JS overhead

Requirements

  • Angular 20+
  • RxJS 7+ (peer dependency, only needed if you use toast.promise() with an Observable)

Installation

npm install ngx-herald
# or
pnpm add ngx-herald

Setup

Add the outlet component once at the root of your application (usually app.component.html):

<!-- app.component.html -->
<ngx-herald />
<router-outlet />

That's it.


Basic usage

Imperative (anywhere in your codebase)

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');

Injectable service (inside Angular components / services)

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.


Toast options

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)
});

ToastOptions reference

OptionTypeDefaultDescription
descriptionstringSecondary line below the message
durationnumber4000Auto-dismiss delay in ms. 0 disables it
positionToastPositionglobal configOverrides the position for this toast only
progressBarbooleantrueShow the countdown bar
dismissiblebooleantrueShow the close button
templateTemplateRefReplaces the default content entirely

Dismiss programmatically

const id = toast.success('File saved');

// dismiss one
toast.dismiss(id);

// dismiss all
toast.dismissAll();

Promise / Observable

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.


Custom templates

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',
  });
}

Template context type

interface ToastTemplateContext {
  $implicit: Toast;  // the full toast object
  dismiss: () => void;
}

Configuration

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
    }),
  ],
};

Positions

'top-left'    | 'top-center'    | 'top-right'
'bottom-left' | 'bottom-center' | 'bottom-right'

Component-level position override

If you need toasts at a specific position regardless of individual toast settings, pass position directly on <ngx-herald>:

<ngx-herald position="top-right" />

Theming

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;
}

Dark mode example

@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;
  }
}

Angular Material override example

: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);
}

Comparison

ngx-heraldngx-toastrAngular Material Snackbar
Angular 20+ / zoneless❌ abandoned
Signals-first
Zero dependencies
Imperative API
Injectable service
Promise / Observable
Custom TemplateRefpartial
CSS custom propertiespartial
npm provenance

Contributing

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

License

MIT