Alert
An inline banner provides users with important information without interrupting their current task. It can be used to communicate the result of an action, share a system notice, or provide helpful guidance in context. It is a self-contained message that can include an icon, heading, description, and optional action link.
Dependencies
When to use
Use an alert when you need to inform users that something has happened or provide relevant guidance without interrupting their current task. It can communicate outcomes such as a successful form submission or a failed upload, as well as important information such as upcoming maintenance. Place the alert close to the content or action it relates to so that its purpose is immediately clear.
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/alert.js";
<eun-alert severity="success" heading="Changes saved">
<span slot="description">Your profile has been updated.</span>
</eun-alert>
document.querySelector("eun-alert").addEventListener("eunclose", () => {
console.log("Alert dismissed");
});
Importing the file registers <eun-alert> as a custom element, with no
further setup needed. It works with any framework, or none, since it's a
standard web component.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/alert.js";
</script>
<eun-alert severity="success" heading="Changes saved">
<span slot="description">Your profile has been updated.</span>
</eun-alert>
<script>
document.querySelector("eun-alert").addEventListener("eunclose", () => {
console.log("Alert dismissed");
});
</script>
npm install @eunomia/elements
import "@eunomia/elements/alert.js";
function ProfileSaved() {
return (
<eun-alert
severity="success"
heading="Changes saved"
oneunclose={() => console.log("Alert dismissed")}
>
<span slot="description">Your profile has been updated.</span>
</eun-alert>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/alert.js";
export function ProfileSaved() {
return (
<eun-alert
severity="success"
heading="Changes saved"
oneunclose={() => console.log("Alert dismissed")}
>
<span slot="description">Your profile has been updated.</span>
</eun-alert>
);
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/alert.js";
</script>
<template>
<eun-alert
severity="success"
heading="Changes saved"
@eunclose="() => console.log('Alert dismissed')"
>
<span slot="description">Your profile has been updated.</span>
</eun-alert>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/alert.js";
@Component({
selector: "app-profile-saved",
template: `
<eun-alert
severity="success"
heading="Changes saved"
(eunclose)="onClose()"
>
<span slot="description">Your profile has been updated.</span>
</eun-alert>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class ProfileSavedComponent {
onClose() {
console.log("Alert dismissed");
}
}
Alternatives
eun-modal with variant="alert"Guidance
- Choose
severityto match the true nature of the message. The icon, color, andalert/statusrole all follow from it automatically - Name the situation in the
headingtext itself ("Payment failed", "Changes saved"). Never rely on icon or color alone to carry that meaning, see the Accessibility tab - Place the alert immediately above or inside the content it relates to (a form, a page section) so its context is unambiguous
- Keep
descriptionactionable : explain what happened and, if something is expected of the user, what to do next - Reserve
closeablefor transient, safely-dismissible notices. For something the user must act on before continuing, use a modal instead (see Alternatives above)
- Using
warning/criticalfor anything that isn't actually broken or urgent. Theirrole="alert"interrupts the screen reader's current speech, so overusing it is disruptive - Leaving
severityunset without also setting a customiconand checking contrast yourself, see "Custom colors" in the Examples tab and the Accessibility tab - Writing a
headingso short or vague ("Oops!", "Notice") that the actual situation only comes through via color or icon - Stacking many alerts permanently on a page "just in case". Each
status/alertrole adds to what a screen reader user has to sit through on every page visit
Live testing
Properties
Alert <eun-alert>
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 alert, matching the nature of the information conveyed. It also drives the default icon and whether the alert interrupts screen reader speech. Leave it unset only for a fully custom look built from the alert's CSS custom properties and a custom icon |
| appearance | 'default' | 'flat' | 'fill' | 'outline' | 'default' | The surface style of the alert. Default and flat share the same tinted surface, with default alone adding a thin leading accent |
| heading | string | — | The alert'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 | false | Shows a close button. Clicking it fires the close event without changing the alert's own rendering |
Import the exact TypeScript type behind any property above, see
Slots
| Name | Description |
|---|---|
| (default) | The alert's heading text, while the heading property is unset. Once heading is set, this instead becomes the alert's description content, rendered as a plain block with no margin or padding — a lighter alternative to the description slot for plain text |
| 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, giving the user more context. Combines with richText if both are set |
| action | An optional action below the description, typically a button or a link |
Events
| Name | Type | Description |
|---|---|---|
| eunclose | CloseEvent | Fired when the close button is activated. The alert has no visibility state of its own, so removing or hiding it in response is the consumer's own responsibility |
Every event above follows the same naming convention, covered in
CSS custom properties
| Name | Description |
|---|---|
| --alert-background-color | Sets the background color when severity is unset |
| --alert-border-color | Sets the border color when severity is unset, for the outline appearance |
| --alert-color | Sets the heading and icon color when severity is unset. The icon always matches this unless alert-icon-color overrides it |
| --alert-icon-color | Sets the icon color only, independently of the heading. Defaults to matching alert-color or the severity accent |
| --alert-content-color | Sets the color of the description and action content, independently of the heading and icon color |
| --alert-border-radius | Sets the corner radius of the alert |
| --alert-padding | Sets the padding of the alert |
| --alert-gap | Sets the gap between the icon column and the heading and content column |
| --alert-row-gap | Sets the gap between the heading, description, and action rows |
| --alert-focus-outline-color | Sets the focus outline color of the close button |
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).
<eun-alert severity="info" heading="New feature available">
<span slot="description">Try it from Settings > Beta features.</span>
</eun-alert>
<eun-alert severity="success" heading="Changes saved">...</eun-alert>
<eun-alert severity="warning" heading="Your trial ends in 3 days"
>...</eun-alert
>
<eun-alert severity="critical" heading="Payment failed">...</eun-alert>
<eun-alert severity="neutral" heading="Scheduled maintenance">...</eun-alert>
Part of the shared vocabulary covered in
Appearances
appearance changes the surface treatment, while severity still drives the icon and
color underneath all four.
<eun-alert severity="info" appearance="default" heading="Default"
>...</eun-alert
>
<eun-alert severity="info" appearance="flat" heading="Flat">...</eun-alert>
<eun-alert severity="info" appearance="outline" heading="Outline"
>...</eun-alert
>
<eun-alert severity="info" appearance="fill" heading="Fill">...</eun-alert>
default's leading accent is rendered as a background gradient rather than
an actual border. A real border adds its own width to the box (or needs
box-sizing compensation to avoid one), while a gradient never shifts
layout at all. outline sits an opaque --eun-surface-color behind its
tinted border so it's never see-through against whatever it's placed on.
Heading only
description is entirely optional — a heading alone is a complete alert.
<eun-alert severity="success" heading="Link copied to clipboard"></eun-alert>
Default slot instead of heading
Without a heading prop, the default slot's content is used instead. This is handy
when the message is already coming from elsewhere as markup rather than a plain string.
<eun-alert severity="warning">
Your session will expire in <strong>2 minutes</strong>.
</eun-alert>
Without icon
<eun-alert severity="info" heading="New feature available" hide-icon>
...
</eun-alert>
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.
<eun-alert icon="stars" heading="You're on the Pro plan"> ... </eun-alert>
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.
<eun-alert heading="You're on the Pro plan">
<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>
...
</eun-alert>
Rich text
richText accepts a small Markdown-like subset (- item lists, [label](url) links, **bold**, *italic*) rendered above the description slot, so both can combine. It's the same engine (and the same non-standard "Markdown-lite" syntax) as formatting toolbar and richText. See
Reach for it specifically when the content is a plain string arriving at runtime that you have no HTML to author for, such as an API error message or a CMS-driven notice, and you never want to build an HTML string yourself to render it (no unsafeHTML, so arbitrary content can never inject markup). When you're writing the content by hand in your own template instead, slot real markup into description directly (see "Link inside the description" below). It skips the Markdown-lite subset and its limitations entirely, at the cost of writing actual HTML instead of a plain string.
<eun-alert
severity="critical"
heading="Payment failed"
rich-text="Your card was declined. [Update your payment method](#) to avoid a service interruption."
></eun-alert>
In practice richText more often comes from data fetched at runtime (an API error message, a CMS-driven notice) than from a string baked into the markup. Set it as a property from code once that data is available, for the same result as the static attribute above :
<eun-alert id="payment-failed" severity="critical" heading="Payment failed">
</eun-alert>
const response = await fetch("/api/payment/retry", { method: "POST" });
if (!response.ok) {
const { message, updateUrl } = await response.json();
document.querySelector("#payment-failed").richText =
`${message} [Update your payment method](${updateUrl}) to avoid a service interruption.`;
}
Link inside the description
richText's [label](url) syntax is enough for most inline links, but slotting a real eun-link directly into description instead works too, and gives full control over it (a target, a leading icon, theme, any other eun-link prop) at the cost of writing real markup instead of a plain string. See "Rich text" above for the opposite case, a plain string arriving at runtime rather than markup you write yourself.
<eun-alert severity="critical" heading="Payment failed">
<span slot="description"
>Your card was declined.
<eun-link href="#">Update your payment method</eun-link>
to avoid a service interruption.</span
>
</eun-alert>
Unlike the dedicated link slot below (a standalone action after the description), this eun-link is just one more piece of the description slot's own content. eun-alert doesn't know it's there, so none of its color handling (e.g. the automatic white recoloring under appearance="fill", see "Action link" below) applies to it. Its default (light) theme already reads fine against every non-fill appearance here ; set theme="dark" (or its own --link-color) by hand if it ends up on an appearance="fill" alert instead.
Action link
<eun-alert severity="warning" heading="Your trial ends in 3 days">
<span slot="description">Upgrade to keep access to Pro features.</span>
<eun-link slot="action" href="#">Upgrade now</eun-link>
</eun-alert>
No theme prop needed on the slotted eun-link itself in either case : its own default (light) theme already reads fine against this appearance="default" alert's light tinted background, and eun-alert automatically re-colors a slotted eun-link to white under appearance="fill" instead. See "Appearances" above, and try switching the example there to appearance="fill" with a link slotted in.
Closeable
Clicking the close button fires eunclose ; eun-alert has no open/closed
state of its own; removing it from the DOM (and moving focus somewhere
sensible) is left to your own handler, shown here.
<eun-alert
id="closeable-alert"
severity="neutral"
heading="Closeable alert"
closeable
>
<span slot="description">...</span>
</eun-alert>
document.querySelector("#closeable-alert").addEventListener("eunclose", () => {
document.querySelector("#closeable-alert").remove();
});
Dynamically added
role="status"/role="alert" only reliably announce content that changes after the page's initial render. An alert already sitting in the markup on load is just read in normal document order, like any other text (see the Accessibility tab). Appending one at runtime, as a real notification would be, is what actually exercises the live region.
saveButton.addEventListener("click", async () => {
await saveChanges();
const alert = document.createElement("eun-alert");
alert.severity = "success";
alert.heading = "Changes saved";
alert.closeable = true;
alert.addEventListener("eunclose", () => alert.remove());
target.appendChild(alert);
});
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. --alert-color sets the heading, and the icon defaults to matching it too. Set --alert-icon-color on top for an icon shade independent of the heading (used here for a slightly brighter accent than the heading text) ; --alert-content-color is separate again, for the description/link text.
<eun-alert
icon="group_add"
heading="You've been invited to a workspace"
style="
--alert-background-color: #f4effa;
--alert-color: #4a2e83;
--alert-icon-color: #6b3fc0;
--alert-content-color: #382a52;
"
>
<span slot="description">Acme Inc. invited you to join their workspace.</span>
</eun-alert>
Full list of custom properties in the API tab.
Keyboard interactions
| Key | Action |
|---|---|
Tab / Shift+Tab |
Moves focus to/from the close button, when closeable is set |
Space / Enter |
Activates the focused close button, native <button> behavior |
eun-alert itself is never focusable and carries no tabindex : it isn't
an interactive control, it's a message. When closeable is unset there is
nothing in it to reach with the keyboard at all.
Live region role : alert vs status
eun-alert renders its content inside a wrapper with role="alert" for
warning/critical severity, and role="status" for info, success,
neutral, or no severity at all, chosen automatically, with no ARIA to
wire up yourself. These aren't just labels : both roles imply a live region
per the
alert rolestatus role
| Role | Implicit aria-live |
Behavior |
|---|---|---|
alert |
assertive |
Interrupts whatever the screen reader is currently announcing |
status |
polite |
Queued, announced once the screen reader finishes its current speech |
Both also imply aria-atomic="true" : the entire message is (re-)announced
as one unit, not just the part that changed. This is why warning/
critical are reserved for messages that genuinely need to interrupt the
user (see "Avoid" in the Overview tab). assertive announcements are
disruptive by design, and using them for routine confirmations trains users
to tune them out.
Static vs. dynamically added alerts
Per
role="alert". The role only earns its keep once the alert (or
its content) is inserted, or an existing one's content changes, in response
to something happening client-side, a form submission result, a real-time
notification. See "Dynamically added" in the Examples tab, which
demonstrates the distinction directly : the static severities above it are
just read in order, while that one is actually announced by a screen reader
the moment the button is activated.
Icon is decorative
The severity/custom icon renders via eun-icon, which always sets
aria-hidden="true" on itself (see its own Accessibility tab). It's never
announced, and never a substitute for wording. This is exactly why the next
point matters.
Don't rely on color or icon alone
eun-alert pairs every severity with a matching color and icon, but the
icon is decorative (see above) and color perception varies (color-blindness,
grayscale display, a screen reader user perceiving neither at all). The
heading/description text is the only reliably-perceived carrier of
meaning. State the nature of the message in words ("Payment failed", not
just a red background and an icon) exactly as called out in the Overview
tab's guidance.
Close button semantics
The close button is a real eun-button (ghost rounded size="s", an
icon-only eun-icon child) positioned top-right, the same building block
and placement eun-modal uses for its own close (X) button. It inherits a
native, fully accessible <button> under the hood with no extra work here,
see ariaLabelClose (default
"Close") is applied as its accessible name, since an icon-only button has
no visible text of its own to derive one from ; set it to something more
specific ("Dismiss trial reminder") when several closeable alerts could
appear on the same page and a generic "Close" wouldn't disambiguate them by
sound alone. Its color always matches --alert-color (the severity accent,
or white under appearance="fill"), not a generic neutral gray, so it never reads
as visually disconnected from the alert it belongs to.
Reading order places the close button first
closeable renders the close button as the first element in the DOM,
ahead of the icon and heading, even though it displays top-right, the same
convention eun-modal uses for its own close button. This is deliberate,
not an oversight : a dismiss control is typically expected to be reachable
immediately rather than after tabbing through the entire message first, and
matches how most screen reader users and sighted keyboard users alike expect
a persistent top-corner action to behave. position: absolute removes it
from the visual grid flow without affecting this DOM/reading order, so
sighted layout and the accessibility tree stay deliberately decoupled here
per
Focus management on close is your responsibility
eun-alert dispatches eunclose and stops there, it never removes or
hides itself (see "Closeable" in the Examples tab). If your eunclose
handler removes the element from the DOM while focus was still on its close
button, the browser drops focus to <body>, and a keyboard user loses their
place per
Color contrast
Every built-in appearance/severity combination is built from this design
system's own color tokens, already chosen to meet contrast minimums for
their paired background. A custom
--alert-background-color/--alert-color/--alert-icon-color/--alert-content-color
combination (severity unset, see "Custom colors" in the Examples tab)
must still be checked against
--alert-icon-color
still needs a sanity check against the background for the same reason any
meaningful-looking graphic should stay legible. --alert-content-color
needs its own check independently of --alert-color/--alert-icon-color :
the description/link body text is a deliberately separate pair against the
same background, so fixing contrast for the heading doesn't guarantee it
for the body copy. A slotted eun-link inherits a white --link-color
automatically under appearance="fill" (see "Action link" in the Examples tab),
precisely so it doesn't silently fail contrast against
a solid, possibly dark, --alert-background-color fill.
No motion
eun-alert has no open/close transition or animation of its own. It
renders fully in place immediately, so there's nothing that needs to respect
prefers-reduced-motion in the first place, unlike
Reference links
alert role
status role