Toast
Stack-managed notification toasts with auto-dismiss, variant-colored indicators, configurable position, and smooth enter/exit animations.
<arc-toast> Overview
Toast provides a stack-managed notification system that surfaces brief, non-blocking messages to the user. Unlike modals or alerts, toasts appear in a fixed corner of the viewport and dismiss themselves automatically, making them ideal for confirming background operations — file saved, record updated, network reconnected — without interrupting the user's workflow.
A single <arc-toast> element acts as the toaster: you place it once in your layout and call its show() method imperatively whenever a notification needs to appear. Each call pushes a new toast onto the stack. Multiple toasts stack vertically with consistent spacing, and each one exits with a scale-and-fade animation after the configured duration. This imperative API keeps your template clean — there is no need to manage an array of open notifications in your component state.
Queueing is built in. max-visible (default 3) caps how many toasts are on screen at once; the rest wait and appear as slots free up, with queue-limit bounding the backlog. Set max-visible="0" for unbounded stacking. dedupe collapses a repeat of a message already showing into a "(×N)" counter on the existing toast — updated in place, so nothing flickers — and restarts its timer, so a message that keeps repeating stays on screen while it does. arc-queue-change reports the visible and queued counts; arc-queue-overflow fires when the backlog is full and the oldest queued toast is dropped.
show() returns the id it assigned, and dismiss(id) removes that toast whether it is visible or still queued.
Toasts can also be raised from anywhere without a reference to the element: dispatch an arc-toast event on document with the same options show() takes.
Progress mode covers long-running work. Pass a numeric progress to show() and the toast renders a track beneath its message, then exempts itself from the two behaviours that assume a message is momentary: it is never deduped, and it never auto-dismisses. Two uploads of a file with the same name are two uploads, so coalescing them would leave one bar tracking both; and the toast ends when the work does, not when a timer says so. Move the bar with updateToast(id, { progress }) — which can revise the message in the same call — and finish with complete(id), which dismisses it and fires arc-complete. Supplying an onCancel callback turns the close button into a cancel button and fires arc-cancel; without one the toast keeps an ordinary dismiss. complete is deliberately not dismiss: the operation finishing and the user closing the toast are different events, and code waiting on the first should not be woken by the second. The mode is chosen at show() and cannot be switched on later — a track appearing mid-life would relayout a notification the reader is already reading.
An action button turns a toast into an undo or a retry. actionLabel renders a ghost button in the toast, and a click runs the action callback, fires arc-action, and dismisses. Both the callback and the event exist because a callback cannot be attached declaratively, and either is a valid way to listen.
Four built-in variants — info, success, warning, and error — apply a colored bottom-edge indicator and a matching icon so users can parse the severity at a glance. The six position options let you anchor the toast stack to any corner or center-edge of the viewport, and a responsive breakpoint ensures toasts span the full width on small screens. The container carries role="status" and aria-live="polite" so screen readers announce new messages without stealing focus.
Guidelines
When to use
- Place a single <arc-toast> element at the root of your layout so all pages share one toaster
- Use the success variant to confirm completed actions like saves, uploads, and deletions
- Keep messages short — one sentence or less — so users can read them before auto-dismiss
- Use the error variant for failures that need acknowledgment but not a blocking dialog
- Set duration to 0 for critical messages that the user must dismiss manually
- Pair with form submissions and async operations to provide immediate feedback
- Use progress mode for work with a knowable percentage — uploads, exports, batch jobs
- Give a progress toast an onCancel whenever the work can actually be abandoned, so the button means something
- Call complete(id) when the work finishes, so listeners can tell completion from the user closing the toast
When not to use
- Do not create multiple <arc-toast> elements on the same page — use one shared instance
- Do not use toasts for information that requires user decision or input; use a Dialog, or Confirm for a yes/no
- Do not display sensitive data (passwords, tokens) in a toast — they are visible to anyone nearby
- Do not set very short durations (under 2 000 ms); users may not have time to read the message
- Do not rely solely on color to convey meaning — the icon and message text must stand on their own
- Do not fire toasts in rapid succession for batch operations; summarize into a single notification
- Do not use progress mode for work of unknown duration — a bar that cannot advance honestly is a Spinner
- Do not leave a progress toast open after its work ends; it never auto-dismisses, so complete(id) or dismiss(id) is required
- Do not put the only route to an irreversible action in a toast action button — it dismisses on its own
Features
- Imperative show() API — call with message, variant, and optional duration; returns the toast id
- `max-visible` caps on-screen toasts (default 3) and queues the rest; `queue-limit` bounds the backlog
- `dedupe` collapses a repeated message into a "(×N)" counter, updated in place
- `dismiss(id)` removes a toast whether it is visible or still queued
- Progress mode — pass `progress` to `show()` for a track that skips dedupe and never auto-dismisses
- `updateToast(id, { progress, message })` moves the bar and revises the text; `complete(id)` ends it and fires `arc-complete`
- `onCancel` turns the close button into a cancel button and fires `arc-cancel`
- `action` and `actionLabel` render an undo/retry button that fires `arc-action` before dismissing
- Document-level `arc-toast` event raises a toast without a reference to the element
- `arc-queue-change` and `arc-queue-overflow` report queue state
- Four variants (info, success, warning, error) with color-coded bottom indicators and icons
- Six position anchors: top-right, top-left, top-center, bottom-right, bottom-left, bottom-center
- Auto-dismiss after configurable duration (default 4 000 ms); pass 0 to persist
- Smooth enter/exit animations with scale and opacity transitions
- Manual dismiss via close button on each toast
- Vertical stacking with consistent gap for multiple simultaneous toasts
- aria-live="polite" container for screen-reader announcements
- Respects `prefers-reduced-motion` — disables animations when set
- Responsive full-width layout on viewports under 640 px
- `arc-close` event fires when a toast is removed
Preview
Usage
This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.
<script type="module" src="@arclux/arc-ui"></script>
<arc-toast id="toaster" position="top-right"></arc-toast>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<arc-button variant="primary"
onclick="document.getElementById('toaster').show({ message: 'Changes saved successfully.', variant: 'success' })">
Success
</arc-button>
<arc-button variant="secondary"
onclick="document.getElementById('toaster').show({ message: 'Something went wrong.', variant: 'error' })">
Error
</arc-button>
<arc-button variant="ghost"
onclick="document.getElementById('toaster').show({ message: 'Deployment in progress...', variant: 'warning', duration: 6000 })">
Warning (6 s)
</arc-button>
<arc-button variant="ghost" onclick="upload()">Upload</arc-button>
</div>
<script>
// Progress mode: a numeric progress option renders the track. The toast then
// skips dedupe and never auto-dismisses — it ends when complete() says so.
async function upload() {
const toaster = document.getElementById('toaster');
const controller = new AbortController();
const id = toaster.show({
message: 'Uploading report.pdf…',
progress: 0,
onCancel: () => controller.abort(), // this is what renders the cancel button
});
try {
for (let sent = 0; sent <= 100; sent += 10) {
await sendChunk(sent, { signal: controller.signal });
toaster.updateToast(id, { progress: sent });
}
toaster.updateToast(id, { message: 'Uploaded report.pdf' });
toaster.complete(id); // dismisses and fires arc-complete
} catch {
toaster.dismiss(id);
toaster.show({ message: 'Upload failed.', variant: 'error' });
}
}
// Completion and cancellation are distinct events, so a listener can tell
// "the work finished" from "the user closed it".
document.getElementById('toaster').addEventListener('arc-complete', (e) => {
console.log('upload finished', e.detail.id);
});
</script> import { Toast, Button } from '@arclux/arc-ui-react';
import type { ArcToast } from '@arclux/arc-ui/toast';
import { useRef } from 'react';
export function NotificationDemo() {
// Typing the ref as ArcToast is what makes show()/updateToast()/complete()
// and the shape of the options object visible to TypeScript.
const toastRef = useRef<ArcToast>(null);
const showSuccess = () =>
toastRef.current?.show({ message: 'Changes saved successfully.', variant: 'success' });
const showError = () =>
toastRef.current?.show({ message: 'Something went wrong.', variant: 'error' });
// Progress mode. show() returns the id every later call needs, and the toast
// stays until complete() or dismiss() — there is no timer to race.
const upload = async () => {
const toaster = toastRef.current;
if (!toaster) return;
const controller = new AbortController();
const id = toaster.show({
message: 'Uploading report.pdf…',
progress: 0,
onCancel: () => controller.abort(),
});
for (let sent = 0; sent <= 100; sent += 10) {
await sendChunk(sent, { signal: controller.signal });
toaster.updateToast(id, { progress: sent });
}
toaster.complete(id);
};
return (
<>
<Toast ref={toastRef} position="top-right" />
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<Button variant="primary" onClick={showSuccess}>Success</Button>
<Button variant="secondary" onClick={showError}>Error</Button>
<Button variant="ghost" onClick={upload}>Upload</Button>
</div>
</>
);
} <script setup>
import { ref } from 'vue';
import { Button, Toast } from '@arclux/arc-ui-vue';
const toaster = ref(null);
const showSuccess = () => toaster.value?.show({ message: 'Changes saved successfully.', variant: 'success' });
const showError = () => toaster.value?.show({ message: 'Something went wrong.', variant: 'error' });
</script>
<template>
<Toast ref="toaster" position="top-right" />
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<Button variant="primary" @click="showSuccess">Success</Button>
<Button variant="secondary" @click="showError">Error</Button>
</div>
</template> <script>
import { Button, Toast } from '@arclux/arc-ui-svelte';
let toaster;
const showSuccess = () => toaster?.show({ message: 'Changes saved successfully.', variant: 'success' });
const showError = () => toaster?.show({ message: 'Something went wrong.', variant: 'error' });
</script>
<Toast bind:this={toaster} position="top-right" />
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<Button variant="primary" on:click={showSuccess}>Success</Button>
<Button variant="secondary" on:click={showError}>Error</Button>
</div> import { Component, ViewChild, ElementRef } from '@angular/core';
import { Button, Toast } from '@arclux/arc-ui-angular';
import type { ArcToast } from '@arclux/arc-ui/toast';
@Component({
imports: [Button, Toast],
template: `
<arc-toast #toaster position="top-right"></arc-toast>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<arc-button variant="primary" (click)="showSuccess()">Success</arc-button>
<arc-button variant="secondary" (click)="showError()">Error</arc-button>
</div>
`,
})
export class NotificationDemoComponent {
@ViewChild('toaster') toaster!: ElementRef<ArcToast>;
showSuccess() {
this.toaster.nativeElement.show({ message: 'Changes saved successfully.', variant: 'success' });
}
showError() {
this.toaster.nativeElement.show({ message: 'Something went wrong.', variant: 'error' });
}
} import { Button, Toast } from '@arclux/arc-ui-solid';
import type { ArcToast } from '@arclux/arc-ui/toast';
export function NotificationDemo() {
let toaster: ArcToast | undefined;
return (
<>
<Toast ref={toaster} position="top-right" />
<div style={{ display: 'flex', gap: '8px', 'flex-wrap': 'wrap' }}>
<Button variant="primary"
onClick={() => toaster?.show({ message: 'Changes saved successfully.', variant: 'success' })}>
Success
</Button>
<Button variant="secondary"
onClick={() => toaster?.show({ message: 'Something went wrong.', variant: 'error' })}>
Error
</Button>
</div>
</>
);
} import { Button, Toast } from '@arclux/arc-ui-preact';
import type { ArcToast } from '@arclux/arc-ui/toast';
import { useRef } from 'preact/hooks';
export function NotificationDemo() {
const toastRef = useRef<ArcToast>(null);
const showSuccess = () =>
toastRef.current?.show({ message: 'Changes saved successfully.', variant: 'success' });
const showError = () =>
toastRef.current?.show({ message: 'Something went wrong.', variant: 'error' });
return (
<>
<Toast ref={toastRef} position="top-right" />
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<Button variant="primary" onClick={showSuccess}>Success</Button>
<Button variant="secondary" onClick={showError}>Error</Button>
</div>
</>
);
} API
-
durationnumber4000 - Time in milliseconds before a toast auto-dismisses. Applies as the default for every show() call but can be overridden per-toast via the duration option in the show() payload. Set to 0 to disable auto-dismiss entirely, requiring the user to click the close button.
-
max-visiblenumber3 - Maximum toasts on screen at once (attribute: max-visible). Further show() calls queue FIFO and release as visible toasts dismiss. Set to 0 for no cap.
-
position'top-right' | 'top-left' | 'top-center' | 'bottom-right' | 'bottom-left' | 'bottom-center''top-right' - Anchors the toast stack to a fixed edge of the viewport. Top-right is the most conventional position for web applications. Bottom positions work well for media players or editors where the top area is occupied by toolbars.
-
dedupebooleantrue - When true, a show() whose message and variant match a visible or queued toast is coalesced: the existing toast gains a "(×N)" counter and a fresh timer instead of a second toast appearing. Set the property to false from JS to disable.
-
queueLimitnumber20 - Maximum queued (not visible) toasts (attribute: queue-limit). Beyond it the oldest queued entries are dropped and arc-queue-overflow fires with the drop count.
Methods
-
show(options?)options?: { id?: number|string, message?: string, variant?: 'info' | 'success' | 'warning' | 'error', duration?: number, persistent?: boolean, progress?: number, action?: () => void, actionLabel?: string, onCancel?: () => void }number|string - Show a toast, or coalesce it into an identical one that is already showing.
Passing
progressputs the toast in progress mode, absorbed from arc-progress-toast in 4.2: it renders a track beneath the message, exempts itself from dedupe and from the auto-dismiss timer, and — givenonCancel— offers a cancel button that firesarc-cancel. Move the bar withupdateToast(id, { progress })and end it withcomplete(id), which firesarc-complete. The mode is chosen here and is not switchable afterwards: a track appearing mid-life would relayout a notification the reader is already reading.actionandactionLabelarrive from arc-snackbar the same way. The label renders a ghost button; a click runs the callback and firesarc-actionbefore the toast dismisses. -
dismiss(id)id: number|string - Dismiss a toast by the id show() returned, whether it is visible or still queued. Unknown ids are ignored.
-
clear() - Dismiss everything on screen and discard the queue.
-
updateToast(id, changes)id: number|string, changes: { progress?: number, message?: string } - Move a progress toast's bar, its message, or both. Unknown ids are ignored,
matching dismiss().
Named
updateToastrather thanupdatebecauseupdateis Lit's — the name arc-progress-toast used, and the reason it also carried a do-nothingupdate(changedProps) { super.update(changedProps); }override that read as if it meant something. -
complete(id)id: number|string - Finish a progress toast: dismiss it and fire
arc-complete. Distinct fromdismiss()on purpose — the operation finishing and the user closing the toast are different events, and a consumer waiting on the first should not be woken by the second.
Events
-
arc-queue-overflow - Fired when the queue exceeds queueLimit and the oldest queued entries are dropped. detail: { dropped }.
-
arc-queue-change - Fired whenever the visible or queued count changes. detail: { visible, queued }.
-
arc-complete - Fired when a progress toast is completed with complete(id). detail: { id }.
-
arc-cancel - Fired when the user clicks a progress toast's cancel button. detail: { id }.
-
arc-action - Fired when the user clicks a toast's action button, before it dismisses. detail: { id }. Absorbed from arc-snackbar, whose action was reachable as an event as well as the
actioncallback — a callback cannot be attached declaratively. -
arc-close - Fired when a toast notification is dismissed. detail: { id } — the id show() returned.