// 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.

aria-required intermediate wcag-2.2-aa

// 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.

Dismissible alerts:

New feature

We just shipped inline code comments. See what's new.

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):

Payment failed. Please try again.

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

HTML — Persistent alert (exists at page load)
<!-- 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>
HTML — Injecting a new alert after load
<!-- 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>
JavaScript — Dismiss button
document.querySelectorAll('[data-dismiss]').forEach(function (btn) {
  btn.addEventListener('click', function () {
    var target = document.getElementById(btn.getAttribute('data-dismiss'));
    if (target) target.remove();
  });
});
CSS
.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
Rule of thumb If the user must see or act on the message, use an alert — don't risk them missing it in a toast that auto-dismisses. If it's a confirmation that the action worked, toast is fine.

// 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 implicitly aria-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 implicitly aria-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).
No focus trap Unlike a modal, an alert is inline and does not trap focus. Users can tab past it freely. If an error alert requires user attention, set focus to it or to the first invalid form field rather than trapping focus.

// 06 · wcag 2.2 success criteria

WCAG 2.2 Success Criteria

This pattern satisfies the following WCAG 2.2 success criteria:

// 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" or role="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

Setting 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.
Color-only status A red-background alert with no icon and no word "Error" is invisible to users who can't see color or who view the page in dark mode with a color filter. Always pair color with a recognizable icon and a text label.
Unlabeled dismiss button <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".
Using 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.
Rendering the alert empty and then populating it An empty alert region at load, populated with text a moment later, may or may not be announced depending on the screen reader's handling of mutations. Insert the fully-populated element as a child of a stable live region for predictable results.
Moving focus to the alert on error For most inline alerts, moving focus is aggressive and unexpected. Prefer a live-region announcement. The exception: form validation errors, where moving focus to the first invalid field (or an error summary) is the clearest way to help the user recover.