<bpdm/ui />
Messages

Toast

Transient notifications fired imperatively from anywhere — variants, descriptions, actions, promises, and six dock positions, in React (@bpdm/ui) and Angular (@bpdm/ng).

The Toast is a transient notification. Fire one imperatively from anywhere — no wrapping the callsite in state — and render the toaster once near your app root. Variants, an optional description and action, swipe-to-dismiss, six dock positions, and a one-call promise(...) helper for async flows. In React you call toast(...) and render <Toaster />; in Angular you inject(BpdmToast) and render <bpdm-toaster />.

Setup

Render a single toaster near your app root — it listens to the global store and renders every toast. Everything else is just firing toasts from your code.

Mount one <Toaster /> near the root; then call toast(...) anywhere — no provider or hook needed.

app.tsx
import { Toaster } from '@bpdm/ui/toast';

export function App() {
  return (
    <>
      <YourApp />
      <Toaster position="bottom-right" />
    </>
  );
}
save-button.tsx
import { toast } from '@bpdm/ui/toast';
import { Button } from '@bpdm/ui/button';

export function SaveButton() {
  return <Button onClick={() => toast.success('Workspace settings updated')}>Save</Button>;
}

Render one <bpdm-toaster /> in the root component template; then inject(BpdmToast) in any component or service and call its methods.

app.ts
import { Component } from '@angular/core';
import { BpdmToaster } from '@bpdm/ng';

@Component({
  selector: 'app-root',
  imports: [BpdmToaster],
  templateUrl: './app.html',
})
export class App {}
app.html
<your-app />
<bpdm-toaster position="bottom-right" />
save-button.ts
import { Component, inject } from '@angular/core';
import { BpdmToast, BpdmButton } from '@bpdm/ng';

@Component({
  selector: 'save-button',
  imports: [BpdmButton],
  template: `<button bpdmButton (click)="save()">Save</button>`,
})
export class SaveButton {
  private toast = inject(BpdmToast);
  save() {
    this.toast.success('Workspace settings updated');
  }
}

Usage

toast-usage.tsx
import { toast } from '@bpdm/ui/toast';
import { Button } from '@bpdm/ui/button';

export function Example() {
  return <Button onClick={() => toast('Workspace settings updated')}>Show toast</Button>;
}
toast-usage.ts
import { Component, inject } from '@angular/core';
import { BpdmToast, BpdmButton } from '@bpdm/ng';

@Component({
  selector: 'toast-usage',
  imports: [BpdmButton],
  template: `<button bpdmButton (click)="show()">Show toast</button>`,
})
export class Example {
  private toast = inject(BpdmToast);
  show() {
    this.toast.show('Workspace settings updated');
  }
}

Variants

success, error, warning, and info each carry their own icon and accent colour; the plain default call is the neutral variant.

toast-variants.tsx
import { toast } from '@bpdm/ui/toast';
import { Button } from '@bpdm/ui/button';

export function Example() {
  return (
    <>
      <Button onClick={() => toast('Workspace updated')}>Default</Button>
      <Button onClick={() => toast.success('Invite sent')}>Success</Button>
      <Button onClick={() => toast.error('Couldn’t save changes')}>Error</Button>
      <Button onClick={() => toast.warning('Storage almost full')}>Warning</Button>
      <Button onClick={() => toast.info('Sync finished')}>Info</Button>
    </>
  );
}
toast-variants.ts
import { Component, inject } from '@angular/core';
import { BpdmToast, BpdmButton } from '@bpdm/ng';

@Component({
  selector: 'toast-variants',
  imports: [BpdmButton],
  templateUrl: './toast-variants.html',
})
export class Example {
  private toast = inject(BpdmToast);
  default() {
    this.toast.show('Workspace updated');
  }
  success() {
    this.toast.success('Invite sent');
  }
  error() {
    this.toast.error('Couldn’t save changes');
  }
  warning() {
    this.toast.warning('Storage almost full');
  }
  info() {
    this.toast.info('Sync finished');
  }
}
toast-variants.html
<button bpdmButton (click)="default()">Default</button>
<button bpdmButton (click)="success()">Success</button>
<button bpdmButton (click)="error()">Error</button>
<button bpdmButton (click)="warning()">Warning</button>
<button bpdmButton (click)="info()">Info</button>

With description

Pass a description for a secondary line under the title.

toast-description.tsx
import { toast } from '@bpdm/ui/toast';
import { Button } from '@bpdm/ui/button';

export function Example() {
  return (
    <Button
      onClick={() =>
        toast.success('Deployment complete', {
          description: 'Build #482 is live in production.',
        })
      }
    >
      Deploy
    </Button>
  );
}
toast-description.ts
import { Component, inject } from '@angular/core';
import { BpdmToast, BpdmButton } from '@bpdm/ng';

@Component({
  selector: 'toast-description',
  imports: [BpdmButton],
  template: `<button bpdmButton (click)="deploy()">Deploy</button>`,
})
export class Example {
  private toast = inject(BpdmToast);
  deploy() {
    this.toast.success('Deployment complete', {
      description: 'Build #482 is live in production.',
    });
  }
}

With action

Add an action (onClick) for a one-tap follow-up — an Undo, a Retry, a View.

toast-action.tsx
import { toast } from '@bpdm/ui/toast';
import { Button } from '@bpdm/ui/button';

export function Example() {
  return (
    <Button
      onClick={() =>
        toast('Member removed', {
          description: 'Jonas Weber no longer has access.',
          action: { label: 'Undo', onClick: () => toast.success('Restored Jonas’s access') },
        })
      }
    >
      Remove member
    </Button>
  );
}
toast-action.ts
import { Component, inject } from '@angular/core';
import { BpdmToast, BpdmButton } from '@bpdm/ng';

@Component({
  selector: 'toast-action',
  imports: [BpdmButton],
  template: `<button bpdmButton (click)="remove()">Remove member</button>`,
})
export class Example {
  private toast = inject(BpdmToast);
  remove() {
    this.toast.show('Member removed', {
      description: 'Jonas Weber no longer has access.',
      action: { label: 'Undo', onClick: () => this.toast.success('Restored Jonas’s access') },
    });
  }
}

Async flows

Drive a toast through an async task with a single callpromise() shows a loading toast, then swaps it in place to success or error when the promise settles (no manual add-then-clear). success / error take a string or a function of the resolved value / error.

toast-promise.tsx
import { toast } from '@bpdm/ui/toast';
import { Button } from '@bpdm/ui/button';

export function Example() {
  const deploy = () => new Promise<void>((resolve) => setTimeout(resolve, 1800));
  return (
    <Button
      onClick={() =>
        toast.promise(deploy(), {
          loading: 'Deploying…',
          success: 'Deployment complete',
          error: 'Deploy failed',
        })
      }
    >
      Deploy
    </Button>
  );
}
toast-promise.ts
import { Component, inject } from '@angular/core';
import { BpdmToast, BpdmButton } from '@bpdm/ng';

@Component({
  selector: 'toast-promise',
  imports: [BpdmButton],
  template: `<button bpdmButton (click)="deploy()">Deploy</button>`,
})
export class Example {
  private toast = inject(BpdmToast);
  deploy() {
    const task = new Promise<void>((resolve) => setTimeout(resolve, 1800));
    this.toast.promise(task, {
      loading: 'Deploying…',
      success: 'Deployment complete',
      error: 'Deploy failed',
    });
  }
}

Duration & persistence

Toasts auto-dismiss after duration ms (the toaster default is 4000). Pass a per-toast duration, or Infinity to keep it until the user dismisses it. A thin countdown bar tracks the remaining time and pauses while the pointer is over the toast, so it never vanishes mid-read.

toast-duration.tsx
import { toast } from '@bpdm/ui/toast';
import { Button } from '@bpdm/ui/button';

export function Example() {
  return (
    <>
      <Button onClick={() => toast('Saved', { duration: 1500 })}>Short (1.5s)</Button>
      <Button
        onClick={() =>
          toast.info('Heads up', {
            description: 'This stays until you dismiss it.',
            duration: Infinity,
          })
        }
      >
        Persistent
      </Button>
    </>
  );
}
toast-duration.ts
import { Component, inject } from '@angular/core';
import { BpdmToast, BpdmButton } from '@bpdm/ng';

@Component({
  selector: 'toast-duration',
  imports: [BpdmButton],
  templateUrl: './toast-duration.html',
})
export class Example {
  private toast = inject(BpdmToast);
  short() {
    this.toast.show('Saved', { duration: 1500 });
  }
  persistent() {
    this.toast.info('Heads up', {
      description: 'This stays until you dismiss it.',
      duration: Infinity,
    });
  }
}
toast-duration.html
<button bpdmButton (click)="short()">Short (1.5s)</button>
<button bpdmButton (click)="persistent()">Persistent</button>

Dismiss

dismiss(id) closes one toast; dismiss() clears them all. Reuse an id to update a toast in place instead of stacking a new one.

toast-dismiss.tsx
import { toast } from '@bpdm/ui/toast';
import { Button } from '@bpdm/ui/button';

export function Example() {
  return (
    <>
      <Button
        onClick={() => {
          toast.info('First notification');
          toast.success('Second notification');
        }}
      >
        Show two
      </Button>
      <Button onClick={() => toast.dismiss()}>Dismiss all</Button>
    </>
  );
}
toast-dismiss.ts
import { Component, inject } from '@angular/core';
import { BpdmToast, BpdmButton } from '@bpdm/ng';

@Component({
  selector: 'toast-dismiss',
  imports: [BpdmButton],
  templateUrl: './toast-dismiss.html',
})
export class Example {
  private toast = inject(BpdmToast);
  showTwo() {
    this.toast.info('First notification');
    this.toast.success('Second notification');
  }
  dismissAll() {
    this.toast.dismiss();
  }
}
toast-dismiss.html
<button bpdmButton (click)="showTwo()">Show two</button>
<button bpdmButton (click)="dismissAll()">Dismiss all</button>

Positions

The dock corner is set once on the toaster via position — one of top-left, top-center, top-right, bottom-left, bottom-center, or bottom-right.

toaster-position.tsx
import { Toaster } from '@bpdm/ui/toast';

// every toast docks here
export function AppToaster() {
  return <Toaster position="top-center" />;
}
app.html
<!-- every toast docks here -->
<bpdm-toaster position="top-center" />

In these docs the toaster is docked top-right, so the toasts you trigger above appear there.

Across navigation

To confirm an action after a redirect — the flash message pattern (submit on one page, see the result on the next) — stash the message before navigating and fire it when the destination mounts. The toast store is global and the toaster lives at the app root, so for in-app client navigation a toast already persists across the route change; stashing also makes it survive a full page reload.

flash.ts
// page A — before navigating
sessionStorage.setItem('flash', JSON.stringify({ type: 'success', message: 'Order placed' }));

// page B — fire it once, on mount
import { useEffect } from 'react';
import { toast } from '@bpdm/ui/toast';

export function useFlash() {
  useEffect(() => {
    const raw = sessionStorage.getItem('flash');
    if (!raw) return;
    sessionStorage.removeItem('flash'); // clear so it only fires once
    const { type, message } = JSON.parse(raw) as { type: 'success' | 'error'; message: string };
    toast[type](message);
  }, []);
}
flash.ts
// page A — before navigating
sessionStorage.setItem('flash', JSON.stringify({ type: 'success', message: 'Order placed' }));
destination.ts
// page B — fire it once, on mount
import { Component, OnInit, inject } from '@angular/core';
import { BpdmToast } from '@bpdm/ng';

@Component({
  selector: 'destination-page',
  template: `<!-- destination content -->`,
})
export class DestinationPage implements OnInit {
  private toast = inject(BpdmToast);
  ngOnInit() {
    const raw = sessionStorage.getItem('flash');
    if (!raw) return;
    sessionStorage.removeItem('flash'); // clear so it only fires once
    const { type, message } = JSON.parse(raw) as { type: 'success' | 'error'; message: string };
    this.toast[type](message);
  }
}

Internationalization

Toast has almost no built-in copy of its own — every title, description, action label, and promise(...) message is author-supplied, so you translate them in your own app exactly like the rest of the content you pass in. The two strings the component owns are the close (X) button's accessible name and the toast region's label, set via a single messages object — a Partial<ToastMessages> merged over the English default:

  • dismiss — the close (X) button's accessible name (aria-label). Default "Dismiss".

  • regionLabel — the accessible label for the toast live-region / viewport landmark (React's Radix Toast.Provider/Toast.Viewport label and Angular's role="region" list). Default "Notifications".

  • React — pass messages to <Toaster messages={{ dismiss: '…' }} />.

  • Angular — bind [messages] on <bpdm-toaster>, or provide it app-wide with provideBpdmToastMessages({ dismiss: '…' }) (the BPDM_TOAST_MESSAGES token).

Toasts mirror under dir="rtl" — the accent bar and countdown move to the inline-start edge and the close (X) to the inline-end; the six dock positions stay physical corners.

The example below localises only the built-in dismiss label to German via messages — the toasts' own titles and descriptions stay in your app's content:

toast-i18n.tsx
import { Toaster, type ToastMessages } from '@bpdm/ui/toast';

// Only the built-in dismiss label is translated; your own copy stays as-is.
const messages: Partial<ToastMessages> = {
  dismiss: 'Schließen',
};

export function AppToaster() {
  return <Toaster position="bottom-right" messages={messages} />;
}
app.ts
import { Component } from '@angular/core';
import { BpdmToaster, type ToastMessages } from '@bpdm/ng';

@Component({
  selector: 'app-root',
  imports: [BpdmToaster],
  templateUrl: './app.html',
})
export class App {
  // Only the built-in dismiss label is translated; your own copy stays as-is.
  readonly messages: Partial<ToastMessages> = {
    dismiss: 'Schließen',
  };
}
app.html
<bpdm-toaster position="bottom-right" [messages]="messages" />

API

toast(title, options?) / BpdmToast

Import { toast } from @bpdm/ui/toast. Call toast(title, options?) for the default variant, plus toast.success / toast.error / toast.warning / toast.info (same signature), toast.dismiss(id?), and toast.promise(promise, messages).

MethodSignatureDescription
toast(title, options?) => stringDefault variant; returns the toast id.
toast.success / .error / .warning / .info(title, options?) => stringSame signature, per-variant icon + accent.
toast.dismiss(id?) => voidClose one by id, or all if omitted.
toast.promise(promise, { loading, success, error }) => PromiseLoading toast that swaps in place on settle.

inject(BpdmToast) — the injectable service is the Angular equivalent of React's callable toast. React's toast(title) maps to this.toast.show(title); every other method matches by name.

MethodSignatureDescription
show(title, opts?) => stringDefault variant; returns the toast id. Same as React's toast(...).
success / error / warning / info(title, opts?) => stringSame signature, per-variant icon + accent.
dismiss(id?) => voidClose one by id, or all if omitted.
promise<T>(promise, { loading, success, error }) => Promise<T>Loading toast that swaps in place on settle.

promise(promise, { loading, success, error })success / error may be a string or a function of the resolved value / error.

ToastOptions

Both frameworks share the same options object.

OptionTypeDefaultDescription
descriptionReact ReactNode / Angular stringSecondary line under the title.
variantdefault | success | error | warning | infodefaultIcon + accent colour.
durationnumber4000Auto-dismiss in ms; Infinity / 0 keeps it open.
action{ label: string; onClick: () => void }Action button.
iconReact ReactNode | null / Angular TemplateRef | nullvariant iconOverride the leading icon; null hides it.
dismissiblebooleantrueShow the close button.
onDismiss() => voidFires when the toast closes.
idstringautoReuse to update an existing toast in place.

Toaster

<Toaster> from @bpdm/ui/toast — render once near the app root.

PropTypeDefaultDescription
positionToastPositionbottom-rightDock corner (six options).
durationnumber4000Default auto-dismiss for all toasts.
messagesPartial<ToastMessages>EnglishOverride the translatable strings (see below).
classNamestringExtra classes on the viewport.

<bpdm-toaster> (BpdmToaster) — render once near the app root. No className input.

InputTypeDefaultDescription
[position]ToastPositionbottom-rightDock corner (six options).
[duration]number4000Default auto-dismiss for all toasts.
[messages]Partial<ToastMessages>EnglishOverride the translatable strings (see below).

App-wide copy — messages

Localise the one built-in string — the close (X) button's accessible name — in a single place (see Internationalization). It's a Partial<ToastMessages> merged over the English default.

  • React — the messages prop on <Toaster>.
  • Angular — the [messages] input on <bpdm-toaster>, or provideBpdmToastMessages({ dismiss: '…' }) (the BPDM_TOAST_MESSAGES token) to set it app-wide.
ToastMessages keyDefaultDescription
dismissDismissAccessible name (aria-label) for the close (X) button.
regionLabelNotificationsAccessible label for the toast live-region / viewport landmark.

Accessibility

  • Each toast is a live regionerror toasts announce assertively (role="alert" / aria-live="assertive"), while every other variant is polite (role="status"), so assistive tech announces them without stealing focus.
  • The toast viewport is a labelled region / list, and the close and action controls inside each toast are real focusable <button>s with a visible keyboard focus ring.
  • The leading icon is decorative (aria-hidden) — the variant is conveyed by the visible title and description text, never by colour or the icon alone.
  • Auto-dismiss pauses on hover and focus, so a toast never vanishes mid-read while it's being pointed at or keyboard-focused.
  • Swipe-to-dismiss is a pointer affordance only — it never strands keyboard users, who close a toast with its focusable close button.
  • Keep titles short and meaningful — they're what gets announced. Put detail in the description.
  • For messages the user must act on, set duration: Infinity so the toast doesn't disappear before it's read; never rely on a toast alone for critical, must-not-miss information.
  • Everything mirrors under dir="rtl" — the accent bar and countdown move to the inline-start edge and the close (X) to the inline-end.

On this page