Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Make notifications accessible

Toast notifications and alerts are announced to screen readers using ARIA live regions and appropriate roles.

Utilities
Quick take
Typical fix time 25 min
  • Use aria-live regions to announce dynamic content changes
  • Choose between 'polite' (waits) and 'assertive' (interrupts) based on urgency
  • Ensure notifications persist long enough to be read
  • Provide visible and programmatic dismiss options
Why it matters: Without proper ARIA attributes, screen reader users miss critical notifications like form errors, success messages, and real-time updates—leaving them unaware of important page changes.

Rule Details

Accessible notifications ensure all users receive important feedback about actions and state changes.

Code Example

<!-- Status notification (polite) -->
<div role="status" aria-live="polite" class="notification">
  Your changes have been saved.
</div>
 
<!-- Alert notification (assertive) -->
<div role="alert" aria-live="assertive" class="notification notification--error">
  Error: Please fill in all required fields.
</div>
 
<!-- Live region container (content injected dynamically) -->
<div
  id="notifications"
  aria-live="polite"
  aria-atomic="true"
  class="sr-only"
></div>

Why It Matters

Without proper ARIA attributes, screen reader users miss critical notifications like form errors, success messages, and real-time updates—leaving them unaware of important page changes.

ARIA Live Region Types

AttributeBehaviorUse For
aria-live="polite"Waits for user pauseStatus updates, non-urgent info
aria-live="assertive"Interrupts immediatelyErrors, time-sensitive alerts
role="status"Implicit politeProgress, success messages
role="alert"Implicit assertiveErrors, warnings

React Toast Component

import { useEffect, useRef } from 'react'
 
type NotificationType = 'success' | 'error' | 'warning' | 'info'
 
interface ToastProps {
  message: string
  type: NotificationType
  duration?: number
  onDismiss: () => void
}
 
export function Toast({
  message,
  type,
  duration = 5000,
  onDismiss
}: ToastProps) {
  const toastRef = useRef<HTMLDivElement>(null)
 
  // Auto-dismiss after duration
  useEffect(() => {
    if (duration > 0) {
      const timer = setTimeout(onDismiss, duration)
      return () => clearTimeout(timer)
    }
  }, [duration, onDismiss])
 
  // Focus toast for keyboard users
  useEffect(() => {
    toastRef.current?.focus()
  }, [])
 
  const isError = type === 'error' || type === 'warning'
 
  return (
    <div
      ref={toastRef}
      role={isError ? 'alert' : 'status'}
      aria-live={isError ? 'assertive' : 'polite'}
      aria-atomic="true"
      tabIndex={-1}
      className={`toast toast--${type}`}
    >
      <span className="toast__icon" aria-hidden="true">
        {type === 'success' && '✓'}
        {type === 'error' && '✕'}
        {type === 'warning' && '⚠'}
        {type === 'info' && 'ℹ'}
      </span>
 
      <span className="toast__message">{message}</span>
 
      <button
        type="button"
        onClick={onDismiss}
        aria-label="Dismiss notification"
        className="toast__dismiss"
      >
        ×
      </button>
    </div>
  )
}

Toast Container with Live Region

import { createContext, useContext, useState, useCallback } from 'react'
 
interface Notification {
  id: string
  message: string
  type: NotificationType
  duration?: number
}
 
interface ToastContextType {
  addToast: (notification: Omit<Notification, 'id'>) => void
  removeToast: (id: string) => void
}
 
const ToastContext = createContext<ToastContextType | null>(null)
 
export function ToastProvider({ children }: { children: React.ReactNode }) {
  const [toasts, setToasts] = useState<Notification[]>([])
 
  const addToast = useCallback((notification: Omit<Notification, 'id'>) => {
    const id = Math.random().toString(36).substr(2, 9)
    setToasts(prev => [...prev, { ...notification, id }])
  }, [])
 
  const removeToast = useCallback((id: string) => {
    setToasts(prev => prev.filter(t => t.id !== id))
  }, [])
 
  return (
    <ToastContext.Provider value={{ addToast, removeToast }}>
      {children}
 
      {/* Toast container with live region */}
      <div
        className="toast-container"
        aria-label="Notifications"
      >
        {toasts.map(toast => (
          <Toast
            key={toast.id}
            message={toast.message}
            type={toast.type}
            duration={toast.duration}
            onDismiss={() => removeToast(toast.id)}
          />
        ))}
      </div>
 
      {/* Screen reader announcement region */}
      <div
        role="status"
        aria-live="polite"
        aria-atomic="true"
        className="sr-only"
      >
        {toasts.length > 0 && toasts[toasts.length - 1].message}
      </div>
    </ToastContext.Provider>
  )
}
 
export function useToast() {
  const context = useContext(ToastContext)
  if (!context) throw new Error('useToast must be used within ToastProvider')
  return context
}

Usage Example

function SaveButton() {
  const { addToast } = useToast()
 
  const handleSave = async () => {
    try {
      await saveData()
      addToast({
        message: 'Changes saved successfully',
        type: 'success',
        duration: 3000
      })
    } catch (error) {
      addToast({
        message: 'Failed to save changes. Please try again.',
        type: 'error',
        duration: 0 // Don't auto-dismiss errors
      })
    }
  }
 
  return <button onClick={handleSave}>Save</button>
}

Inline Notifications

interface InlineNotificationProps {
  type: 'error' | 'warning' | 'success' | 'info'
  title?: string
  children: React.ReactNode
  dismissible?: boolean
  onDismiss?: () => void
}
 
export function InlineNotification({
  type,
  title,
  children,
  dismissible = false,
  onDismiss
}: InlineNotificationProps) {
  const isUrgent = type === 'error' || type === 'warning'
 
  return (
    <div
      role={isUrgent ? 'alert' : 'status'}
      aria-live={isUrgent ? 'assertive' : 'polite'}
      className={`notification notification--${type}`}
    >
      {title && (
        <strong className="notification__title">{title}</strong>
      )}
      <div className="notification__content">{children}</div>
 
      {dismissible && (
        <button
          type="button"
          onClick={onDismiss}
          aria-label="Dismiss"
          className="notification__dismiss"
        >
          ×
        </button>
      )}
    </div>
  )
}

Progress Notifications

function UploadProgress({ progress, fileName }: { progress: number; fileName: string }) {
  return (
    <div
      role="status"
      aria-live="polite"
      aria-busy={progress < 100}
      className="upload-progress"
    >
      <span className="sr-only">
        Uploading {fileName}: {progress}% complete
      </span>
 
      <div aria-hidden="true">
        <span>{fileName}</span>
        <progress value={progress} max="100" />
        <span>{progress}%</span>
      </div>
    </div>
  )
}

Timing Guidelines

Notification TypeRecommended Duration
Success messages3-5 seconds
Info/status5-7 seconds
Warnings8-10 seconds or manual dismiss
ErrorsNo auto-dismiss (manual only)
const DURATION_MAP = {
  success: 3000,
  info: 5000,
  warning: 8000,
  error: 0, // No auto-dismiss
} as const

Styling

.toast-container {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
 
.toast {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 1rem;
  border-radius: 0.5rem;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  animation: slideIn 0.3s ease-out;
}
 
.toast--success { background: #d4edda; border-left: 4px solid #28a745; }
.toast--error { background: #f8d7da; border-left: 4px solid #dc3545; }
.toast--warning { background: #fff3cd; border-left: 4px solid #ffc107; }
.toast--info { background: #d1ecf1; border-left: 4px solid #17a2b8; }
 
.toast__dismiss {
  background: none;
  border: none;
  font-size: 1.25rem;
  cursor: pointer;
  padding: 0.25rem;
  margin-left: auto;
}
 
/* Screen reader only */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}
 
@keyframes slideIn {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}
 
/* Respect motion preferences */
@media (prefers-reduced-motion: reduce) {
  .toast {
    animation: none;
  }
}

Verification

  1. Enable screen reader and trigger notifications
  2. Verify announcements occur at appropriate times
  3. Test keyboard dismissal (Escape key)
  4. Check notifications don't disappear too quickly
  5. Verify focus management after dismissal
  6. Test with different screen readers (NVDA, VoiceOver, JAWS)
Don't Overuse Assertive

Reserve aria-live="assertive" for truly urgent messages. Overuse disrupts the user experience by constantly interrupting screen reader output.

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Verify notifications use aria-live regions, appropriate role (alert or status), and persist long enough to be read.

Fix

Auto-fix issues

Implement notifications with role='alert' or role='status', aria-live='polite' or 'assertive', and adequate display time.

Explain

Learn more

Explain how accessible notifications ensure all users are informed of important updates regardless of how they interact with the page.

Review

Code review

Review templates, server-rendered HTML, and shared components that output markup related to Make notifications accessible. Flag exact elements, attributes, and routes where the rendered HTML violates the rule.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

Nu Html Checker
validator.w3.orgTool

Rules that often go hand-in-hand with this one.

Make carousels accessible

Carousels and sliders are accessible with pause controls, keyboard navigation, and proper ARIA attributes.

Accessibility
Create accessible tooltips

Tooltips are accessible to keyboard users and screen readers with proper ARIA attributes and focus handling.

Accessibility
Announce dynamic content with ARIA live regions

Dynamic content updates are announced to screen readers using appropriate ARIA live regions.

Accessibility
Make custom elements and Web Components accessible

Custom elements must implement ARIA reflection via ElementInternals, keyboard interaction, and form association so that screen readers and assistive technologies can interpret them correctly.

HTML

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385