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.
import { Toaster } from '@bpdm/ui/toast';
export function App() {
return (
<>
<YourApp />
<Toaster position="bottom-right" />
</>
);
}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.
import { Component } from '@angular/core';
import { BpdmToaster } from '@bpdm/ng';
@Component({
selector: 'app-root',
imports: [BpdmToaster],
templateUrl: './app.html',
})
export class App {}<your-app />
<bpdm-toaster position="bottom-right" />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
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>;
}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.
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>
</>
);
}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');
}
}<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.
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>
);
}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.
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>
);
}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 call — promise() 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.
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>
);
}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.
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>
</>
);
}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,
});
}
}<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.
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>
</>
);
}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();
}
}<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.
import { Toaster } from '@bpdm/ui/toast';
// every toast docks here
export function AppToaster() {
return <Toaster position="top-center" />;
}<!-- 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.
// 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);
}, []);
}// page A — before navigating
sessionStorage.setItem('flash', JSON.stringify({ type: 'success', message: 'Order placed' }));// 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 RadixToast.Provider/Toast.Viewportlabel and Angular'srole="region"list). Default"Notifications". -
React — pass
messagesto<Toaster messages={{ dismiss: '…' }} />. -
Angular — bind
[messages]on<bpdm-toaster>, or provide it app-wide withprovideBpdmToastMessages({ dismiss: '…' })(theBPDM_TOAST_MESSAGEStoken).
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:
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} />;
}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',
};
}<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).
| Method | Signature | Description |
|---|---|---|
toast | (title, options?) => string | Default variant; returns the toast id. |
toast.success / .error / .warning / .info | (title, options?) => string | Same signature, per-variant icon + accent. |
toast.dismiss | (id?) => void | Close one by id, or all if omitted. |
toast.promise | (promise, { loading, success, error }) => Promise | Loading 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.
| Method | Signature | Description |
|---|---|---|
show | (title, opts?) => string | Default variant; returns the toast id. Same as React's toast(...). |
success / error / warning / info | (title, opts?) => string | Same signature, per-variant icon + accent. |
dismiss | (id?) => void | Close 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.
| Option | Type | Default | Description |
|---|---|---|---|
description | React ReactNode / Angular string | — | Secondary line under the title. |
variant | default | success | error | warning | info | default | Icon + accent colour. |
duration | number | 4000 | Auto-dismiss in ms; Infinity / 0 keeps it open. |
action | { label: string; onClick: () => void } | — | Action button. |
icon | React ReactNode | null / Angular TemplateRef | null | variant icon | Override the leading icon; null hides it. |
dismissible | boolean | true | Show the close button. |
onDismiss | () => void | — | Fires when the toast closes. |
id | string | auto | Reuse to update an existing toast in place. |
Toaster
<Toaster> from @bpdm/ui/toast — render once near the app root.
| Prop | Type | Default | Description |
|---|---|---|---|
position | ToastPosition | bottom-right | Dock corner (six options). |
duration | number | 4000 | Default auto-dismiss for all toasts. |
messages | Partial<ToastMessages> | English | Override the translatable strings (see below). |
className | string | — | Extra classes on the viewport. |
<bpdm-toaster> (BpdmToaster) — render once near the app root. No className
input.
| Input | Type | Default | Description |
|---|---|---|---|
[position] | ToastPosition | bottom-right | Dock corner (six options). |
[duration] | number | 4000 | Default auto-dismiss for all toasts. |
[messages] | Partial<ToastMessages> | English | Override 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
messagesprop on<Toaster>. - Angular — the
[messages]input on<bpdm-toaster>, orprovideBpdmToastMessages({ dismiss: '…' })(theBPDM_TOAST_MESSAGEStoken) to set it app-wide.
ToastMessages key | Default | Description |
|---|---|---|
dismiss | Dismiss | Accessible name (aria-label) for the close (X) button. |
regionLabel | Notifications | Accessible label for the toast live-region / viewport landmark. |
Accessibility
- Each toast is a live region —
errortoasts 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: Infinityso 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.