Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

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 alert, it isn't placed inline with the content it's about. It floats above the page at a fixed position, stacks automatically with any other toast sharing that position, and, severity permitting, closes itself after a delay. It reuses the same severity palette and appearance options as alert for visual consistency, but is built independently: it doesn't wrap or depend on it.

Dependencies

eun-icon · for the severity or custom icon, unless hidden eun-button · for the close button
Overview API Examples Accessibility
Show toast

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 alert instead, since an alert lives in the page's own layout, while a toast floats independently of it. If the user must respond before continuing at all, use modal.

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.

Alternatives

You want .. Prefers The message to stay put next to what it's about, or be revisited later eun-alert The user must respond before continuing at all eun-modal

Guidance

  • 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 heading short 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 action slot 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 duration on 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 position values across one app without reason: it makes toasts harder to predict and find

Live testing

Properties

Toast <eun-toast>

Attributes

NameTypeDefaultDescription
hide-iconbooleanfalseHides the icon entirely
rich-textstringLightweight formatted content rendered above the description slot, combining with it. Supports simple lists, links, bold, and italic text
aria-label-closestring'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'verticalOverrides 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
headingstringThe toast's heading, as a property. When unset, the default slot's content is used instead
iconEunomiaIconNameA custom icon to display instead of the one implied by severity. Ignored once severity is set
closeablebooleantrueShows a close button. Always shown when severity is critical, so an error toast is never left with no way to dismiss it
durationnumber5000Milliseconds before the toast auto-dismisses. Zero, or a critical severity regardless of this value, disables auto-dismiss entirely, requiring a manual close
openbooleantrueWhether 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 Types.

Slots

NameDescription
(default)The toast's heading text. Ignored once the heading property is set
iconReplaces the icon entirely with any content, not just an icon element. Falls back to the icon implied by severity when empty
descriptionExtra detail below the heading, combining with richText if both are set. A link can be slotted in directly, or included through richText
actionAn optional action below the description, typically a button or a link

Events

NameTypeDescription
euncloseCloseEventFired 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 Events.

CSS custom properties

NameDescription
--toast-z-indexSets the stacking order of the toast
--toast-viewport-gapSets the distance between the toast and the screen edges its position anchors to
--toast-stack-gapSets the gap between stacked toasts sharing the same position
--toast-widthSets the width of the toast
--toast-transition-durationSets the duration of the enter, exit, and stack reflow transitions
--toast-background-colorSets the background color when severity is unset
--toast-border-colorSets the border color when severity is unset, for the outline appearance
--toast-colorSets the heading and icon color when severity is unset. The icon always matches this unless toast-icon-color overrides it
--toast-icon-colorSets the icon color only, independently of the heading. Defaults to matching toast-color or the severity accent
--toast-content-colorSets the color of the description and action content, independently of the heading and icon color
--toast-border-radiusSets the corner radius of the toast
--toast-paddingSets the padding of the toast
--toast-gapSets the gap between the icon column and the heading and content column
--toast-row-gapSets the gap between the heading, description, and action rows
--toast-shadowSets the shadow of the toast
--toast-focus-outline-colorSets 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.

Info Success Warning Critical Neutral
<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 Variants, alongside every other component that reuses it.

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.

Default Flat Outline Fill
<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.

Show without icon
<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.

Show with custom icon
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.

Show with slotted icon
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.

top-left top-center top-right left right bottom-left bottom-center bottom-right
<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.

top-left top-right bottom-left bottom-right
<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.

Save changes
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 Markdown-lite for the full syntax. A link can come from 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.

Charge a card
<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.

Archive conversation
<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.

Show toast
<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.

Show closeable toast
<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.

Show invite toast
<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 WAI-ARIA Alert pattern, since a notification appearing shouldn't hijack whatever the user was already doing.

Live region role: alert vs status

Same convention as eun-alert: 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 WCAG 4.1.3 Status Messages requires for a live region to reliably announce at all.

Timing: auto-dismiss, pausing, and severity="critical"

WCAG 2.2.1 Timing Adjustable requires that time-limited content can be turned off, extended, or paused by the user, since a countdown a screen reader user or someone reading slowly can't stop is a real barrier, not a cosmetic concern. eun-toast satisfies this three ways, in order of how it actually applies:

  1. 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.
  2. 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.
  3. severity="critical" forces it off, regardless of whatever duration was set, since an error is exactly the case where the user must still be able to see and act on the message, so eun-toast doesn'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 eun-alert uses for its own close button: a native, fully accessible <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 WCAG 2.4.3 Focus Order. This mostly comes up when a toast is closed manually while focused (rather than via its own timer, where focus is elsewhere by definition). Move focus somewhere deliberate right after removal in that case.

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 eun-alert's own icon.

Don't rely on color or icon alone

WCAG 1.4.1 Use of Color requires that information conveyed by color also be available another way. State the nature of the message in the 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 WCAG 1.4.3 Contrast (Minimum). See Color for the tokens that already meet it.

Reference links

WCAG 4.1.3: Status Messages
WCAG 2.2.1: Timing Adjustable
WCAG 1.4.1: Use of Color
WCAG 1.4.3: Contrast (Minimum)
WCAG 2.4.3: Focus Order
WAI-ARIA APG: Alert Pattern
WAI-ARIA: alert role
WAI-ARIA: status role
MDN: ARIA live regions