Snackbar Container
Bottom-right toast stack. Drive it imperatively — show(config), the variant shorthands, dismiss(id), and clearAll(). Defaults to info / persistent / dismissable, with uniqueKey dedupe. Messages are announced through the shared announcer, not by the stack itself.
Preview
Place one container on the page, then call its methods: el.success('Saved.').
<button data-kind="success">Success</button>
<esa-snackbar-container id="toasts"></esa-snackbar-container> import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@esa/ecology/esa-snackbar-container';
@Component({
selector: 'app-snackbar-container-demo',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<button data-kind="success">Success</button>
<esa-snackbar-container id="toasts"></esa-snackbar-container>
`,
})
export class SnackbarContainerDemo {} API
Methods
| Prop | Type | Default | Description |
|---|---|---|---|
show(config) | method | — | Shows a toast. config: { message, variant?, duration?, action?, dismissable?, uniqueKey? }. Returns its id. |
success / info / warning / danger(msg, config?) | method | — | Shorthands for show() with that variant. |
dismiss(id) | method | — | Removes the toast with that id. |
clearAll() | method | — | Removes every active toast. |
Config object (show)
| Prop | Type | Default | Description |
|---|---|---|---|
message | string | — | Toast body text. |
variant | 'info' | 'success' | 'warning' | 'danger' | 'info' | Color and leading icon. |
duration | number | 0 | Auto-dismiss delay in ms. 0 (the default) keeps the toast until dismissed. This default changed from 5000 on 2026-08-16 — a timer the user cannot adjust is WCAG 2.2.1 Timing Adjustable, a Level A failure. Passing a duration still works and still auto-dismisses; it opts you out of 2.2.1 unless you provide the adjustment yourself. |
action | string | — | Optional action button label. Discouraged — see Accessibility below. Only safe when the same action is also reachable in the page itself. |
dismissable | boolean | true | Renders the close button. |
uniqueKey | string | — | Dedupe key. A second show() with the same key does not stack a second toast, but it DOES re-announce the message and restart any countdown — the second occurrence is a real event. |
Attributes
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | 'Notifications' | Accessible name for the region landmark that wraps the stack. Change it only if a page has more than one stack. |
Events
| Event | Type | Default | Description |
|---|---|---|---|
snackbar-action | CustomEvent<{ id: string }> | — | Fired when a toast's action button is pressed. |
Accessibility
Toasts are the hardest common pattern to make accessible, and most implementations fail. This one is built persistent-first: the timed, animated version is the enhancement, not the baseline.
The stack is not a live region
Announcing is handled by the shared announcer (@esa/ecology/announcer),
which owns the only two live regions in the kit — one polite, one assertive, in the
light DOM, mounted before anything happens. Putting aria-live on this
stack instead would fail three ways: each toast is created and populated in the same
tick, so the region would be born with its content and never announce; the text sits
two shadow roots deep, where live-region observation is unreliable; and every page
with a stack would own a third region competing with the other two.
What the stack does carry is role="region" with a name, so it is a
landmark. An announcement tells someone a toast exists; the landmark is how they
get to it.
Timing (SC 2.2.1, Level A)
durationnow defaults to0— the toast stays until dismissed. That is the only configuration that conforms without extra work.- If you pass a duration, hovering or focusing the stack pauses every countdown. That helps pointer and keyboard users. It does not help someone reading with a screen reader's virtual cursor, which fires neither event — so pausing is a mitigation, not a conformance argument.
- Esc dismisses the newest toast, but only while focus is inside the stack. A global handler would let someone closing a tooltip destroy a toast they had not read.
Action buttons
action is supported and discouraged. A live region announces raw text
with no roles, so "Undo" is read as a bare word with nothing to say it is a button;
focus is never moved to it; and there is no mechanism to navigate to a toast. Add one
only when the same action is also available in the page. If the toast is the
only route to it, that is not a status message — use
esa-dialog, which moves focus and
persists.
- Dismissing a toast that held focus returns focus to wherever it came from, rather than dropping it to
<body>. - Below 30rem the stack becomes a full-width strip instead of a 420px box overflowing the viewport (SC 1.4.10 Reflow, 1.4.4 Resize).
- Each toast renders an esa-snackbar-item with a 32px dismiss target and a visible focus ring.
- The newest toast appears at the bottom of the stack (column-reverse), nearest the user's attention.
Theming surface
Every public token this component reads, extracted from its source at build time,
split by blast radius: the hooks that belong to this component,
then the shared and system tokens it consumes alongside everything else. The
Resolves to column walks each token's real lineage —
component → semantic → primitive → raw value — so you
can see exactly where re-pointing it lands. A spoke re-skins by overriding these in
its theme-<slug>.css; component internals are never edited. No
hook for what you need? File it with /request-lego.
Wired to this component
No other component reads these — re-point them and esa-snackbar-container is the
only thing that moves.
| Token | Tier | Resolves to |
|---|---|---|
--snackbar-container-max-width | component | 420px |
Shared tokens it reads
Owned by a family or by the semantic layer. Still overridable — but scope the
override (esa-foo { --form-border-color: … }) unless you mean to move
everything that reads it. Reach says what else that is.
| Token | Reach | Resolves to |
|---|---|---|
--z-toast | semantic every component | 500 |
--spacing-200 | primitive every component | 0.5rem |
--spacing-500 | primitive every component | 1.5rem |