// pattern
Accessible Alert / Banner Pattern
Persistent inline messages for status updates, warnings, and errors. Built with the right ARIA live region role, color-independent status, and keyboard-accessible dismiss controls.
// 01 · live demo
Live Demo
Four persistent alert variants, always visible in the DOM.
Heads up
Maintenance window is scheduled for Sunday 2–4am PT. The dashboard will be unavailable during that time.
Success
Your changes have been saved.
Warning
Your subscription ends in 3 days. Renew now to keep your projects active.
Something went wrong
We couldn't process your payment. Check your card details and try again.
Dismissible alerts:
New feature
We just shipped inline code comments. See what's new.
Upload failed
The file exceeded the 10MB limit. Try a smaller file.
Inject a new alert (announced on insertion):
Each click inserts a fresh element with the appropriate role into the live region. Screen readers announce the new alert because the element was added, not just made visible.
Inaccessible version (color-only, no role):
No role, no icon, no text label. Screen reader users hear "Payment failed, please try again" but have no indication that this is an error. Users who can't perceive the red color (low vision, color blindness, dark mode bugs) have no signal at all.
// 02 · the code
The Code
<!-- role="status" for non-urgent messages.
Use role="alert" for urgent/error messages. -->
<div role="status" class="alert alert--success">
<svg class="alert__icon" aria-hidden="true">
<!-- checkmark icon -->
</svg>
<div class="alert__body">
<p class="alert__title">Success</p>
<p>Your changes have been saved.</p>
</div>
</div>
<!-- Dismissible alert with an accessible name
on the close button -->
<div role="status" class="alert alert--info" id="promo-alert">
<svg class="alert__icon" aria-hidden="true"></svg>
<div class="alert__body">
<p class="alert__title">New feature</p>
<p>We just shipped inline code comments.</p>
</div>
<button type="button" class="alert__dismiss"
aria-label="Dismiss new feature message"
data-dismiss="promo-alert">
<svg aria-hidden="true"><!-- close X --></svg>
</button>
</div>
<!-- An empty container that lives in the DOM.
JS will inject alert elements here. -->
<div id="alert-region"></div>
<script>
function announceAlert(type, title, message) {
var region = document.getElementById('alert-region');
var el = document.createElement('div');
// Use role="alert" for errors (assertive);
// role="status" for success (polite).
el.setAttribute('role', type === 'error' ? 'alert' : 'status');
el.className = 'alert alert--' + type;
el.innerHTML =
'<div class="alert__body">' +
'<p class="alert__title">' + title + '</p>' +
'<p>' + message + '</p>' +
'</div>';
region.appendChild(el);
}
</script>
document.querySelectorAll('[data-dismiss]').forEach(function (btn) {
btn.addEventListener('click', function () {
var target = document.getElementById(btn.getAttribute('data-dismiss'));
if (target) target.remove();
});
});
.alert {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 1rem;
border-radius: 0.25rem;
border: 1px solid #d1d5db;
border-left-width: 4px;
background: #f8f9fa;
color: #111827;
font-size: 0.875rem;
}
.alert__icon {
flex-shrink: 0;
width: 1.25rem;
height: 1.25rem;
margin-top: 0.125rem;
}
.alert__body { flex: 1; line-height: 1.5; }
.alert__title {
font-weight: 600;
margin: 0 0 0.25rem;
}
.alert__dismiss {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
/* SC 2.5.8: minimum 24x24 target */
min-width: 1.75rem;
min-height: 1.75rem;
padding: 0;
border: none;
background: transparent;
color: inherit;
border-radius: 0.25rem;
cursor: pointer;
}
.alert__dismiss:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Variants use color, icon, and text label
(not color alone) to satisfy SC 1.4.1 */
.alert--info { border-left-color: var(--color-primary); background: #eff6ff; }
.alert--success { border-left-color: #16a34a; background: #f0fdf4; }
.alert--warning { border-left-color: #d97706; background: #fffbeb; }
.alert--error { border-left-color: #dc2626; background: #fef2f2; }
// 03 · alert vs. toast
Alert vs. Toast
Both use ARIA live regions, but they're used differently.
| Aspect | Alert / Banner (this pattern) | Toast |
|---|---|---|
| Position | Inline, within page flow | Floating overlay (usually corner) |
| Duration | Persistent — stays until dismissed or the issue is resolved | Auto-dismisses after a few seconds |
| Use for | Status of the page/section, actionable errors, feature announcements | Transient confirmations ("Saved", "Copied") |
| Can contain interactive elements? | Yes — dismiss button, action links, "Retry" buttons | Limited — dismiss only; auto-dismiss is hostile to focus |
| Dismissal | User-initiated or on resolution | Timed, with pause on hover/focus |
// 04 · why these decisions
Why These Decisions
Why role="alert" vs role="status"?
They both make the element a live region, but they differ in urgency:
role="alert"is implicitlyaria-live="assertive". Screen readers interrupt whatever they are saying to announce it. Reserve it for urgent, actionable messages — form errors, payment failures, "you have been signed out."role="status"is implicitlyaria-live="polite". Screen readers wait until the current announcement finishes. Use it for success messages, progress updates, and general information.
Overusing role="alert" makes your page feel hostile to screen reader users — every announcement cuts them off.
Why include an icon and a color and a text label?
SC 1.4.1 "Use of Color" prohibits relying on color alone to convey meaning. A green background for "success" is ambiguous to users with color blindness, low vision, or custom stylesheets. Pair every variant with a distinct icon and a word like "Success", "Warning", or "Error" in the message.
Why not put role="alert" on elements that exist at page load?
Live regions only fire when their content changes or a child is added. An element with role="alert" sitting in the initial HTML announces nothing — the screen reader reads it in document order like any other element. If you need the content announced, insert it after the page has loaded.
Why a real <button> for dismiss?
The dismiss button must be keyboard-focusable, have a clear accessible name, and meet the 24×24 target size. A <button> with aria-label="Dismiss warning message" gives you all three. A plain <span> with an X glyph and a click handler gives you none.
// 05 · keyboard interaction
Keyboard Interaction
| Key | Action |
|---|---|
| Tab | Moves focus to any interactive elements inside the alert (action links, dismiss button) in document order. |
| Enter or Space | Activates the focused element (follows a link, dismisses the alert). |
// 06 · wcag 2.2 success criteria
WCAG 2.2 Success Criteria
This pattern satisfies the following WCAG 2.2 success criteria:
- 1.3.1 Info and Relationships Level A — The role communicates the semantic purpose of the region to assistive technology.
- 1.4.1 Use of Color Level A — Status is communicated with an icon and a text label, not color alone.
- 1.4.11 Non-text Contrast Level AA — The accent border and icon have at least 3:1 contrast against the surrounding background.
- 2.1.1 Keyboard Level A — All alert controls (links, dismiss button) are operable via keyboard.
- 2.4.13 Focus Appearance Level AAA — Focused controls have a 2px outline with sufficient contrast.
- 2.5.8 Target Size (Minimum) Level AA — The dismiss button meets the 24×24px minimum.
- 3.3.1 Error Identification Level A — Error alerts identify the problem in text.
- 4.1.3 Status Messages Level AA — Alerts added to the DOM after load are announced via the live region role without moving focus.
// 07 · screen reader behavior
Screen Reader Behavior
When a new alert is inserted
role="alert": the screen reader interrupts current speech and reads the entire contents of the alert.role="status": the announcement is queued and read after the current sentence finishes.
The announcement includes all text inside the region. Keep messages concise — a 3-paragraph alert with role="alert" creates a painful interruption.
When navigating to a persistent alert
- Without a heading or label, the alert is read as part of the normal document flow. Consider adding a bold "Success" or "Error" word as the first text inside the region so it reads naturally.
- A
role="alert"orrole="status"on a page-load element is exposed as a live region in the accessibility tree, but won't re-announce on navigation.
Gotcha: identical text doesn't re-announce
Firing the same "Save failed" alert twice in a row may announce only once. If you need a repeated announcement, append a counter, vary the wording slightly, or remove and re-insert the element with a brief delay. This is covered in detail in the live regions guide.
// 08 · common mistakes
Common Mistakes
role="alert" on an element that exists at page load
The screen reader reads the element in document order — the role does nothing. For welcome banners or persistent messages at load, use role="status" for the semantic meaning, and don't expect an announcement unless you dynamically insert the content after load.
<button><svg/></button> has no name. Screen reader users hear "button" with no indication of what it does. Add aria-label="Dismiss upload error" or include visible text like "Close".
role="alert" for every non-error
Assertive announcements interrupt the user. Save "Your shopping cart is empty" for role="status". Reserve role="alert" for messages that would cause harm if missed.