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
Critical Implementation Note
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.
// Incorrect: Dynamically adding live region
{showAlert && <div role="alert">Message</div>}
// Correct: Live region always exists, content changes
<div role="alert">
{message && <span>{message}</span>}
</div> 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.
WAI-ARIA Roles
-
alert- An element that displays a brief, important message that attracts the user's attention without interrupting their task
WAI-ARIA alert role (opens in new tab)
Implicit ARIA Properties
The role="alert" implicitly sets the following ARIA properties. You do NOT need to add these manually:
| Property | 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
Alerts require no keyboard interaction. They are designed to inform users without interrupting their workflow. The alert content is automatically announced by screen readers when it changes.
If the alert includes a dismiss button, the button follows standard button keyboard interaction:
| Key | Action |
|---|---|
| Enter | Activates the dismiss button |
| Space | Activates the dismiss button |
Focus Management
- 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
tabindexor receive keyboard focus. - Dismiss button is focusable - If present, the dismiss button can be reached via Tab navigation.
Important Guidelines
No Auto-Dismissal
Alerts should NOT disappear automatically. Per WCAG 2.2.3 No Timing (opens in new tab) , users need sufficient time to read content. If auto-dismissal is required:
- Provide user control to pause/extend the display time
- Ensure sufficient display duration (minimum 5 seconds + reading time)
- Consider if the content is truly non-essential
Alert Frequency
Excessive alerts can inhibit usability, especially for users with visual or cognitive disabilities ( WCAG 2.2.4 Interruptions (opens in new tab) ). Use alerts sparingly for truly important messages.
Alert vs Alert Dialog
Use Alert when:
- 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") when:
- The message requires immediate user response
- User must acknowledge or take action before continuing
- Focus should move to the dialog (modal behavior)
Note: role="alertdialog" requires focus management and keyboard handling (Escape to close, focus trapping).
Only use it when modal interruption is appropriate.
Screen Reader Behavior
- Immediate announcement - When alert content changes, screen readers interrupt current reading to announce the alert (
aria-live="assertive"). - Full content announced - The entire alert content is read, not just the changed portion (
aria-atomic="true"). - Initial content not announced - Alerts that exist when the page loads are NOT automatically announced. Only dynamic changes trigger announcements.
References
Source Code
<script setup lang="ts">
import { computed, useId } from "vue";
import { cn } from "@/lib/utils";
import { Info, CircleCheck, AlertTriangle, OctagonAlert, X } from "lucide-vue-next";
import { type AlertVariant, variantStyles } from "./alert-config";
export type { AlertVariant };
export interface AlertProps {
/**
* Alert message content.
* Changes to this prop trigger screen reader announcements.
*/
message?: string;
/**
* Alert variant for visual styling.
* Does NOT affect ARIA - all variants use role="alert"
*/
variant?: AlertVariant;
/**
* Custom ID for the alert container.
* Useful for SSR/hydration consistency.
*/
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;
}
const props = withDefaults(defineProps<AlertProps>(), {
message: undefined,
variant: "info",
id: undefined,
dismissible: false,
class: "",
});
const emit = defineEmits<{
dismiss: [];
}>();
// Generate SSR-safe unique ID
const generatedId = useId();
const alertId = computed(() => props.id ?? `alert-${generatedId}`);
const hasContent = computed(() => Boolean(props.message) || Boolean(slots.default));
const slots = defineSlots<{
default?: () => unknown;
}>();
const variantIcons = {
info: Info,
success: CircleCheck,
warning: AlertTriangle,
error: OctagonAlert,
};
const handleDismiss = () => {
emit("dismiss");
};
</script>
<template>
<div
:class="
cn(
'apg-alert',
hasContent && [
'relative flex items-start gap-3 px-4 py-3 rounded-lg border',
'transition-colors duration-150',
variantStyles[variant],
],
!hasContent && 'contents',
props.class
)
"
>
<!-- Live region - contains only content for screen reader announcement -->
<div
:id="alertId"
role="alert"
:class="cn(
hasContent && 'flex-1 flex items-start gap-3',
!hasContent && 'contents'
)"
>
<template v-if="hasContent">
<span class="apg-alert-icon flex-shrink-0 mt-0.5" aria-hidden="true">
<component :is="variantIcons[variant]" class="size-5" />
</span>
<span class="apg-alert-content flex-1">
<template v-if="message">{{ message }}</template>
<slot v-else />
</span>
</template>
</div>
<!-- Dismiss button - outside live region to avoid SR announcing it as part of alert -->
<button
v-if="hasContent && dismissible"
type="button"
:class="
cn(
'apg-alert-dismiss',
'flex-shrink-0 min-w-11 min-h-11 p-2 -m-2 rounded',
'flex items-center justify-center',
'hover:bg-black/10 dark:hover:bg-white/10',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-current'
)
"
aria-label="Dismiss alert"
@click="handleDismiss"
>
<X class="size-5" aria-hidden="true" />
</button>
</div>
</template> Usage
<script setup>
import { ref } from 'vue';
import Alert from './Alert.vue';
const message = ref('');
function showAlert() {
message.value = 'Operation completed!';
}
function clearAlert() {
message.value = '';
}
</script>
<template>
<!-- IMPORTANT: Alert container is always in DOM -->
<Alert
:message="message"
variant="info"
:dismissible="true"
@dismiss="clearAlert"
/>
<button @click="showAlert">
Show Alert
</button>
</template> API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
message | string | - | Alert message content |
variant | 'info' | 'success' | 'warning' | 'error' | 'info' | Visual style variant |
dismissible | boolean | false | Show dismiss button |
id | string | auto-generated | Custom ID |
class | string | - | Additional CSS classes |
Events
| Event | Description |
|---|---|
@dismiss | Emitted when dismiss button is clicked |
Slots
| Slot | Description |
|---|---|
default | Complex content (alternative to message prop) |
Testing
Testing Overview
The Alert component tests focus on verifying correct live region behavior and APG compliance. The most critical test is ensuring the alert container remains in the DOM when content changes.
Test Categories
High Priority: APG Core Compliance
| 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
| 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
| 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 |
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.
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