Alert
An element that displays a brief, important message in a way that attracts the user's attention without interrupting the user's task.
Demo
Click the buttons below to show alerts with different variants. The live region container exists in the DOM from page load - only the content changes.
Accessibility Features
WAI-ARIA Roles
| Role | Target Element | Description |
|---|---|---|
alert | Alert container | An element that displays a brief, important message that attracts the userโs attention without interrupting their task |
Implicit ARIA Properties
| Attribute | Implicit Value | Description |
|---|---|---|
aria-live | assertive | Interrupts screen reader to announce immediately |
aria-atomic | true | Announces entire alert content, not just changed parts |
Keyboard Support
| Key | Action |
|---|---|
| Enter | Activates the dismiss button (if present) |
| Space | Activates the dismiss button (if present) |
- Screen readers detect changes to live regions by observing DOM mutations inside them. If the live region itself is added dynamically, some screen readers may not announce the content reliably.
Focus Management
| Event | Behavior |
|---|---|
| Alert must NOT move focus | Alerts are non-modal and should not interrupt user workflow by stealing focus |
| Alert container is not focusable | The alert element should not have tabindex or receive keyboard focus |
| Dismiss button is focusable | If present, the dismiss button can be reached via Tab navigation |
Implementation Notes
<!-- Container always in DOM -->
<div role="alert">
<!-- Content added dynamically -->
<span>Your changes have been saved.</span>
</div>
Announcement Behavior:
- Page load content: NOT announced
- Dynamic changes: ANNOUNCED immediately
- aria-live="assertive": interrupts current speech
Alert vs Status:
โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ role="alert"โ role="status" โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโค
โ assertive โ polite โ
โ interrupts โ waits for pause โ
โ urgent info โ non-urgent updates โ
โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโ
Alert component structure and announcement behavior
Use Alert
- The message is informational and doesnโt require user action
- User workflow should NOT be interrupted
- Focus should remain on the current task
Use Alert Dialog (role=โalertdialogโ)
- The message requires immediate user response
- User must acknowledge or take action before continuing
- Focus should move to the dialog (modal behavior)
Important Notes
- The live region container (role=โalertโ) must exist in the DOM from page load. Do NOT dynamically add/remove the container itself. Only the content inside the container should change dynamically.
References
Source Code
<script lang="ts" module>
import type { Snippet } from 'svelte';
import type { AlertVariant } from './alert-config';
export type { AlertVariant };
// Module-level counter for generating unique IDs
let idCounter = 0;
function generateId() {
return `alert-${++idCounter}`;
}
export interface AlertProps {
/**
* Alert message content.
* Changes to this prop trigger screen reader announcements.
*/
message?: string;
/**
* Optional children for complex content.
* Use message prop for simple text alerts.
*/
children?: Snippet;
/**
* Alert variant for visual styling.
* Does NOT affect ARIA - all variants use role="alert"
*/
variant?: AlertVariant;
/**
* Custom ID for the alert container.
* Auto-generated if not provided.
*/
id?: string;
/**
* Whether to show dismiss button.
* Note: Manual dismiss only - NO auto-dismiss per WCAG 2.2.3
*/
dismissible?: boolean;
/**
* Additional class name for the alert container
*/
class?: string;
}
</script>
<script lang="ts">
import { cn } from '@/lib/utils';
import { Info, CircleCheck, AlertTriangle, OctagonAlert, X } from 'lucide-svelte';
import { variantStyles } from './alert-config';
let {
message,
children,
variant = 'info',
id: providedId,
dismissible = false,
class: className = '',
onDismiss,
}: AlertProps & { onDismiss?: () => void } = $props();
// Generate stable ID - use provided or generate once
const generatedId = generateId();
let alertId = $derived(providedId ?? generatedId);
let hasContent = $derived(Boolean(message) || Boolean(children));
let IconComponent = $derived(
variant === 'success'
? CircleCheck
: variant === 'warning'
? AlertTriangle
: variant === 'error'
? OctagonAlert
: Info
);
function handleDismiss() {
onDismiss?.();
}
</script>
<div
class={cn(
'apg-alert',
hasContent && [
'relative flex items-start gap-3 rounded-lg border px-4 py-3',
'transition-colors duration-150',
variantStyles[variant],
],
!hasContent && 'contents',
className
)}
>
<!-- Live region - contains only content for screen reader announcement -->
<div
id={alertId}
role="alert"
class={cn(hasContent && 'flex flex-1 items-start gap-3', !hasContent && 'contents')}
>
{#if hasContent}
<span class="apg-alert-icon mt-0.5 flex-shrink-0" aria-hidden="true">
<IconComponent class="size-5" />
</span>
<span class="apg-alert-content flex-1">
{#if message}
{message}
{:else if children}
{@render children()}
{/if}
</span>
{/if}
</div>
<!-- Dismiss button - outside live region to avoid SR announcing it as part of alert -->
{#if hasContent && dismissible}
<button
type="button"
class={cn(
'apg-alert-dismiss',
'-m-2 min-h-11 min-w-11 flex-shrink-0 rounded p-2',
'flex items-center justify-center',
'hover:bg-black/10 dark:hover:bg-white/10',
'focus:ring-2 focus:ring-current focus:ring-offset-2 focus:outline-none'
)}
aria-label="Dismiss alert"
onclick={handleDismiss}
>
<X class="size-5" aria-hidden="true" />
</button>
{/if}
</div> Usage
<script lang="ts">
import Alert from './Alert.svelte';
let message = $state('');
function showAlert() {
message = 'Operation completed!';
}
function clearAlert() {
message = '';
}
</script>
<!-- IMPORTANT: Alert container is always in DOM -->
<Alert
id="my-alert"
{message}
variant="info"
dismissible
onDismiss={clearAlert}
/>
<button onclick={showAlert}>
Show Alert
</button> API
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | Required ID for SSR consistency |
message | string | - | Alert message content |
children | Snippet | - | Complex content (alternative to message) |
variant | 'info' | 'success' | 'warning' | 'error' | 'info' | Visual style variant |
dismissible | boolean | false | Show dismiss button |
onDismiss | () => void | - | Callback when dismissed |
class | string | - | Additional CSS classes |
Testing
Tests verify APG compliance for live region behavior, ARIA attributes, and accessibility requirements. The Alert component uses a two-layer testing strategy.
Testing Strategy
Unit Tests (Testing Library)
Verify the component's rendered output using framework-specific testing libraries. These tests ensure correct HTML structure and ARIA attributes.
- ARIA attributes (role="alert")
- Live region container persistence in DOM
- Dismiss button accessibility
- Accessibility via jest-axe
E2E Tests (Playwright)
Verify component behavior in a real browser environment across all frameworks. These tests cover interactions and cross-framework consistency.
- ARIA structure in live browser
- Focus management (alert does NOT steal focus)
- Dismiss button keyboard interactions
- Tab navigation behavior
- axe-core accessibility scanning
- Cross-framework consistency checks
Test Categories
High Priority : APG Core Compliance (Unit + E2E)
| Test | APG Requirement |
|---|---|
| role="alert" exists | Alert container must have alert role |
| Container always in DOM | Live region must not be dynamically added/removed |
| Same container on message change | Container element identity preserved during updates |
| Focus unchanged after alert | Alert must not move keyboard focus |
| Alert not focusable | Alert container must not have tabindex |
Medium Priority : Accessibility Validation (Unit + E2E)
| Test | WCAG Requirement |
|---|---|
| No axe violations (with message) | WCAG 2.1 AA compliance |
| No axe violations (empty) | WCAG 2.1 AA compliance |
| No axe violations (dismissible) | WCAG 2.1 AA compliance |
| Dismiss button accessible name | Button has aria-label |
| Dismiss button type="button" | Prevents form submission |
Low Priority : Props & Extensibility (Unit)
| Test | Feature |
|---|---|
| variant prop changes styling | Visual customization |
| id prop sets custom ID | SSR support |
| className inheritance | Style customization |
| children for complex content | Content flexibility |
| onDismiss callback fires | Event handling |
Low Priority : Cross-framework Consistency (E2E)
| Test | Feature |
|---|---|
| All frameworks have alert | React, Vue, Svelte, Astro all render alert element |
| Same trigger buttons | All frameworks have consistent trigger buttons |
| Show alert on click | All frameworks show alert when button is clicked |
Screen Reader Testing
Automated tests verify DOM structure, but manual testing with screen readers is essential for validating actual announcement behavior.
| Screen Reader | Platform |
|---|---|
| VoiceOver | macOS / iOS |
| NVDA | Windows |
| JAWS | Windows |
| TalkBack | Android |
Verify that message changes trigger immediate announcement and that existing content on page load is NOT announced.
Testing Tools
- Vitest (opens in new tab) - Test runner for unit tests
- Testing Library (opens in new tab) - Framework-specific testing utilities (React, Vue, Svelte)
- Playwright (opens in new tab) - Browser automation for E2E tests
- axe-core/playwright (opens in new tab) - Automated accessibility testing in E2E
Running Tests
# Run all Alert tests
npm run test -- alert
# Run tests for specific framework
npm run test -- Alert.test.tsx # React
npm run test -- Alert.test.vue # Vue
npm run test -- Alert.test.svelte # Svelte See testing-strategy.md (opens in new tab) for full documentation.
Resources
- WAI-ARIA APG: Alert Pattern (opens in new tab)
- WAI-ARIA APG: Alert Dialog Pattern (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist