Toast
Toast is a self-dismissing notification anchored to a screen corner,
reporting something that just happened without requiring the user's
attention, such as a save confirmation, a background job's result, or an
undoable action. Unlike
Dependencies
When to use
Reach for a toast to confirm the result of something the user just did,
without making them respond to it, such as a form saved, a file uploaded,
or an item added to a cart. It's transient by nature: it disappears on its
own (unless it's an error, see "Closing" below) and never blocks the page.
If the message needs to stay put next to the thing it's about, or the user
needs to be able to come back to it later, use
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/toast.js";
<eun-toast severity="success" heading="Changes saved">
<span slot="description">Your profile has been updated.</span>
</eun-toast>
document.querySelector("eun-toast").addEventListener("eunclose", () => {
console.log("Toast closed");
});
Importing the file registers <eun-toast> as a custom element, with no
further setup needed. It works with any framework, or none, since it's a
standard web component. In practice a toast is almost always created from
code the moment something happens, rather than baked into the initial
markup. See "Created from code" in the Examples tab.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/toast.js";
</script>
<eun-toast severity="success" heading="Changes saved">
<span slot="description">Your profile has been updated.</span>
</eun-toast>
<script>
document.querySelector("eun-toast").addEventListener("eunclose", () => {
console.log("Toast closed");
});
</script>
npm install @eunomia/elements
import "@eunomia/elements/toast.js";
function ProfileSaved() {
return (
<eun-toast
severity="success"
heading="Changes saved"
oneunclose={() => console.log("Toast closed")}
>
<span slot="description">Your profile has been updated.</span>
</eun-toast>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/toast.js";
export function ProfileSaved() {
return (
<eun-toast
severity="success"
heading="Changes saved"
oneunclose={() => console.log("Toast closed")}
>
<span slot="description">Your profile has been updated.</span>
</eun-toast>
);
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/toast.js";
</script>
<template>
<eun-toast
severity="success"
heading="Changes saved"
@eunclose="() => console.log('Toast closed')"
>
<span slot="description">Your profile has been updated.</span>
</eun-toast>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/toast.js";
@Component({
selector: "app-profile-saved",
template: `
<eun-toast
severity="success"
heading="Changes saved"
(eunclose)="onClose()"
>
<span slot="description">Your profile has been updated.</span>
</eun-toast>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class ProfileSavedComponent {
onClose() {
console.log("Toast closed");
}
}
Alternatives
eun-alerteun-modalGuidance
- Create toasts from code, right when the thing they report actually happens (a save completing, a request failing), not as static markup on page load
- Keep the
headingshort and specific ("Changes saved", not "Success!"), since a toast is glanced at, not read carefully - Pick
severity="critical"deliberately: it forces the toast to stay open until the user dismisses it, exactly because an error shouldn't vanish before they've seen it - Reserve the
actionslot for something directly reversing or following up on what just happened ("Undo", "View"), not a generic navigation link - Pick one `position` per general area of the app (e.g. always `bottom-right`) so users build a reliable expectation of where to look
- Using a toast for anything the user must act on to proceed: see Alternatives above, since a toast can always be missed
- Setting a very short custom
durationon a message with real content to read: see "Timing" in the Accessibility tab for the WCAG minimum this can violate - Stacking many unrelated toasts at once "just in case": each one competes for the same glance of attention
- Mixing several different
positionvalues across one app without reason: it makes toasts harder to predict and find
Live testing
Properties
Toast <eun-toast>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| hide-icon | boolean | false | Hides the icon entirely |
| rich-text | string | — | Lightweight formatted content rendered above the description slot, combining with it. Supports simple lists, links, bold, and italic text |
| aria-label-close | string | 'Close' | The accessible label of the close button |
| severity | 'info' | 'success' | 'warning' | 'critical' | 'neutral' | — | The severity of the toast, matching the nature of the information conveyed. It also drives the default icon and, for critical, the forced persistent behavior. Leave it unset only for a fully custom look built from the toast's CSS custom properties and a custom icon |
| appearance | 'default' | 'flat' | 'fill' | 'outline' | 'default' | The surface style of the toast, using the same names as the alert component. Since a toast floats over arbitrary page content rather than sitting in the page's own flow, its default appearance is an opaque card with a thin leading accent rather than a tinted wash. Flat alone keeps the tinted wash |
| position | 'top-left' | 'top-center' | 'top-right' | 'right' | 'bottom-right' | 'bottom-center' | 'bottom-left' | 'left' | 'bottom-right' | Which screen corner or edge the toast is anchored to. Every toast sharing the same position stacks together automatically, animating out of each other's way as siblings open or close |
| slide | 'vertical' | 'horizontal' | vertical | Overrides the enter and exit slide direction for the four corner positions only. Unset keeps their default vertical slide, and horizontal slides them through their left or right edge instead. Has no effect on the center or side positions |
| heading | string | — | The toast's heading, as a property. When unset, the default slot's content is used instead |
| icon | EunomiaIconName | — | A custom icon to display instead of the one implied by severity. Ignored once severity is set |
| closeable | boolean | true | Shows a close button. Always shown when severity is critical, so an error toast is never left with no way to dismiss it |
| duration | number | 5000 | Milliseconds before the toast auto-dismisses. Zero, or a critical severity regardless of this value, disables auto-dismiss entirely, requiring a manual close |
| open | boolean | true | Whether the toast is currently shown. Set it to false upfront, or hide it right after inserting, to insert a toast that's already closed |
Import the exact TypeScript type behind any property above, see
Slots
| Name | Description |
|---|---|
| (default) | The toast's heading text. Ignored once the heading property is set |
| icon | Replaces the icon entirely with any content, not just an icon element. Falls back to the icon implied by severity when empty |
| description | Extra detail below the heading, combining with richText if both are set. A link can be slotted in directly, or included through richText |
| action | An optional action below the description, typically a button or a link |
Events
| Name | Type | Description |
|---|---|---|
| eunclose | CloseEvent | Fired once the toast has finished closing, whether from the close button, the timer running out, or being hidden programmatically. The toast doesn't remove itself from the DOM, so doing so in response is the consumer's own responsibility |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --toast-z-index | Sets the stacking order of the toast |
| --toast-viewport-gap | Sets the distance between the toast and the screen edges its position anchors to |
| --toast-stack-gap | Sets the gap between stacked toasts sharing the same position |
| --toast-width | Sets the width of the toast |
| --toast-transition-duration | Sets the duration of the enter, exit, and stack reflow transitions |
| --toast-background-color | Sets the background color when severity is unset |
| --toast-border-color | Sets the border color when severity is unset, for the outline appearance |
| --toast-color | Sets the heading and icon color when severity is unset. The icon always matches this unless toast-icon-color overrides it |
| --toast-icon-color | Sets the icon color only, independently of the heading. Defaults to matching toast-color or the severity accent |
| --toast-content-color | Sets the color of the description and action content, independently of the heading and icon color |
| --toast-border-radius | Sets the corner radius of the toast |
| --toast-padding | Sets the padding of the toast |
| --toast-gap | Sets the gap between the icon column and the heading and content column |
| --toast-row-gap | Sets the gap between the heading, description, and action rows |
| --toast-shadow | Sets the shadow of the toast |
| --toast-focus-outline-color | Sets the focus outline color of the close button |
A toast only makes sense fixed to a real screen corner, so every example
below is a button that creates one and appends it to the page, exactly how
eun-toast is meant to be used in practice, not baked into the initial
markup. Nothing needs to be removed manually afterward: eun-toast never
deletes itself, but eunclose (fired once the exit transition actually
finishes, see "Timing" in the Accessibility tab) is the consumer's own cue
to do so, shown in every example below.
Severity
Five severities are available, each pairing a default icon and color and
picking the correct live-region role automatically (alert for
warning/critical, status for the rest, see the Accessibility tab).
critical also disables auto-dismiss entirely, regardless of duration.
Try it below, it's the only one of the five that doesn't disappear on its
own.
<eun-button id="save-trigger">Info</eun-button>
document.querySelector("#save-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.severity = "info";
toast.heading = "New feature available";
const description = document.createElement("span");
description.slot = "description";
description.textContent = "Try it from Settings > Beta features.";
toast.append(description);
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Part of the shared vocabulary covered in
Appearances
appearance changes the surface treatment, while severity still drives
the icon and color underneath all four. Unlike eun-alert's own default
(a tinted wash), eun-toast's default is an opaque --eun-surface-color
card with a 4px leading accent, since a toast floats over arbitrary page
content, so it needs to stay legible against anything behind it, not blend
into a tint of its own severity color.
<eun-button id="appearance-trigger">Outline</eun-button>
document.querySelector("#appearance-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.severity = "info";
toast.appearance = "outline";
toast.heading = "Outline";
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Without icon
hideIcon hides the icon entirely, regardless of severity. The content
column then collapses back to fill the freed space, flush against the
padding, rather than leaving an empty gap where the icon used to be.
<eun-button id="hideicon-trigger">Show without icon</eun-button>
document.querySelector("#hideicon-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.severity = "info";
toast.hideIcon = true;
toast.heading = "New feature available";
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Custom icon
Leave severity unset to take full control of the icon via the icon
prop. Once severity is set, its own mapped icon always takes precedence.
document.querySelector("#customicon-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.icon = "stars";
toast.heading = "You're on the Pro plan";
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Need something other than a eun-icon, or a different icon library
entirely? Slot your own non-interactive content into icon instead. It
replaces the severity/icon-implied icon entirely.
document.querySelector("#slottedicon-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.heading = "You're on the Pro plan";
toast.innerHTML = `
<svg slot="icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m12 2 2.9 6.9L22 10l-5.5 4.8L18 22l-6-3.7L6 22l1.5-7.2L2 10l7.1-1.1L12 2Z"></path>
</svg>
<span slot="description">Enjoy unlimited projects and priority support.</span>
`;
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Position
position anchors the toast to one of eight screen corners/edges. All eight
share the same stacking behavior: any other eun-toast sharing the same
position offsets itself out of the way automatically, most recent one
flush against the edge, older ones animating out of the way to make room
for it, and animating back when it (or any other sibling) closes, with no
wrapping container needed.
The box below stands in for the screen itself: each button sits at the
actual corner/edge it opens a toast against, and contain: layout confines
that toast to the box instead of the real viewport, purely so clicking
around doesn't cover the rest of this page. Drop the contain trick in real
usage, see the code below the demo.
<eun-button id="position-trigger">bottom-right</eun-button>
document.querySelector("#position-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.severity = "neutral";
toast.position = "bottom-right";
toast.heading = "bottom-right";
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Horizontal slide for corners
The four corners (top-left, top-right, bottom-left, bottom-right)
default to the same vertical slide as top-center/bottom-center, but
unlike those, a corner doesn't have a single obviously-correct axis. Set
slide="horizontal" to slide it through its left/right edge instead: left
corners slide in from the left, right corners from the right, same
direction their pure left/right counterparts already use.
<eun-button id="corner-slide-trigger">top-left</eun-button>
document
.querySelector("#corner-slide-trigger")
.addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.severity = "neutral";
toast.position = "top-left";
toast.slide = "horizontal";
toast.heading = "top-left";
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Created from code
This is how a toast is meant to be used in practice: created the moment
something happens, appended to the page, and left to manage its own
lifetime. Nothing needs to be removed manually. eun-toast never deletes
itself, but it also never needs to, since the next one simply replaces it
in the same corner. Click the button a few times in a row to see several
toasts sharing bottom-right stack automatically.
saveButton.addEventListener("click", async () => {
await saveChanges();
const toast = document.createElement("eun-toast");
toast.severity = "success";
toast.heading = "Changes saved";
toast.position = "bottom-right";
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Description, rich text and link
Same richText engine as eun-alert. See
richText's [label](url) syntax, or
from real markup slotted directly into description. Either way it's
inside the description, no separate slot needed for it. severity="critical"
also means this one won't disappear on its own, so it has to be dismissed
by hand, see "Severity" above.
<eun-button id="richtext-trigger">Charge a card</eun-button>
document.querySelector("#richtext-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.severity = "critical";
toast.heading = "Payment failed";
toast.richText =
"Your card was declined. [Update your payment method](#) to avoid a service interruption.";
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Action
The action slot is for a single follow-up action distinct from the close
button, typically an "Undo", matching what triggered the toast in the
first place.
<eun-button id="archive-trigger">Archive conversation</eun-button>
document.querySelector("#archive-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.heading = "Conversation archived";
const undo = document.createElement("eun-button");
undo.slot = "action";
undo.setAttribute("size", "s");
undo.setAttribute("flat", "");
undo.textContent = "Undo";
undo.addEventListener("click", () => toast.hide());
toast.append(undo);
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Auto-dismiss and pausing
Set duration (ms) to control how long a toast stays open before closing
itself. 0 disables auto-dismiss entirely (as does severity="critical",
see "Severity" above). Hovering the toast, or moving keyboard focus inside
it (e.g. into its action), pauses the countdown for as long as that lasts.
See "Timing" in the Accessibility tab for why this matters, not just as a
nicety. Show one below and try hovering it partway through. The default time
is set to 5 seconds.
<eun-button id="duration-trigger">Show toast (4s)</eun-button>
document.querySelector("#duration-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.severity = "success";
toast.heading = "Saved";
toast.duration = 4000;
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Closing
Clicking the close button (or calling hide()) sets open to false and
plays the exit transition. eunclose only fires once that transition has
actually finished, see "Timing" in the Accessibility tab. eun-toast never
removes itself from the DOM. Doing so is the consumer's own responsibility,
same contract as eun-alert's own eunclose. Every example on this page
does it the same way, in its own eunclose listener.
<eun-button id="closeable-trigger">Show closeable toast</eun-button>
document.querySelector("#closeable-trigger").addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.severity = "neutral";
toast.heading = "Closeable toast";
toast.duration = 0;
document.body.appendChild(toast);
// eunclose only fires once the exit transition has actually finished, so
// this removal is never visible.
toast.addEventListener("eunclose", () => toast.remove());
});
Custom colors
Every color is a CSS variable, read only while severity is unset. Set
severity and these are ignored in favor of the built-in palette. Same
scheme as eun-alert's own custom colors: --toast-color sets the
heading (and the icon by default), --toast-icon-color overrides the icon
independently, --toast-content-color is separate again for the
description/action text.
<eun-button id="custom-color-trigger">Show invite toast</eun-button>
document
.querySelector("#custom-color-trigger")
.addEventListener("click", () => {
const toast = document.createElement("eun-toast");
toast.icon = "group_add";
toast.heading = "You've been invited to a workspace";
toast.style.cssText = `
--toast-background-color: #f4effa;
--toast-color: #4a2e83;
--toast-icon-color: #6b3fc0;
--toast-content-color: #382a52;
`;
document.body.appendChild(toast);
toast.addEventListener("eunclose", () => toast.remove());
});
Full list of custom properties in the API tab.
Keyboard interactions
| Key | Action |
|---|---|
Tab / Shift+Tab |
Moves focus to/from the close button and action slot content |
Space / Enter |
Activates the focused close button/action, native <button> behavior |
eun-toast itself is never focusable and carries no tabindex: it isn't
an interactive control, it's a message. It also never moves focus to itself
when it appears, per the
Live region role: alert vs status
Same convention as
warning/
critical severity renders role="alert" (aria-live="assertive"
implied, interrupts current speech), everything else renders
role="status" (aria-live="polite" implied, queued). A toast is one of
the clearest legitimate uses of these roles in the first place. Unlike a
static alert that might sit in the initial markup, a toast is always
inserted after the page has already loaded, which is exactly the condition
Timing: auto-dismiss, pausing, and severity="critical"
eun-toast
satisfies this three ways, in order of how it actually applies:
- Pausing. The countdown pauses for as long as the pointer hovers the
toast, or focus is anywhere inside it (
action, the close button), resuming with whatever time was left rather than restarting. Try it in "Auto-dismiss, and pausing it" in the Examples tab. This alone covers sighted mouse and keyboard users. - Turning it off. Set
duration="0"on any toast that carries content worth reading carefully, or that a screen reader user might land on well after it appeared (its countdown keeps running while unfocused, so a slow-to-arrive screen reader announcement can still land after it's already closed). There's no reliable way for a not-yet-focused screen reader user to "hover" a toast to pause it ahead of time. severity="critical"forces it off, regardless of whateverdurationwas set, since an error is exactly the case where the user must still be able to see and act on the message, soeun-toastdoesn't trust a duration setting for it at all. See "Severity" in the Examples tab.
A default duration (5000ms) is a reasonable amount of time for a short
confirmation, not a substitute for the above on anything longer.
Exit transition and eunclose timing
eunclose doesn't fire the instant a close is requested (button click,
timer expiry, hide()). It waits for the exit transition to actually
finish first (with a short internal fallback in case the transition can't
run at all). This matters for prefers-reduced-motion too: the transition
duration collapses to near-zero rather than being removed outright, so
eunclose still fires through the same code path either way, just
essentially immediately. In practice this means a eunclose handler that
removes the toast from the DOM (see "Closing" in the Examples tab) never
cuts the animation short. By the time it runs, the toast has already
finished disappearing.
Close button semantics
The close button is a real eun-button (ghost rounded size="xs", an
icon-only eun-icon child), the same building block
<button> under the hood with no
extra work here. ariaLabelClose (default "Close") is its accessible
name. Set it to something more specific when several toasts could be
stacked and announced close together and a generic "Close" wouldn't
disambiguate them by sound alone.
Focus management on close is your responsibility
Same as eun-alert: eun-toast dispatches eunclose and stops there. It
never removes or hides itself from the DOM. If your eunclose handler
removes the element while focus was still on its close button, the browser
drops focus to <body>, and a keyboard user loses their place per
Icon is decorative
The severity/custom icon renders via eun-icon, which always sets
aria-hidden="true" on itself, never announced, never a substitute for
wording, exactly like
Don't rely on color or icon alone
heading text itself ("Payment
failed", not just a red background and an icon), same guidance as
eun-alert's own Overview tab.
Color contrast
Every built-in appearance/severity combination reuses this design system's
own color tokens, already chosen to meet contrast minimums for their paired
background. A custom
--toast-background-color/--toast-color/--toast-icon-color/--toast-content-color
combination (severity unset, see "Custom colors" in the Examples tab)
must still be checked against
Reference links
alert role
status role