Alert
ユーザーのタスクを中断せずに、重要なメッセージを目立つ形で表示する要素。
デモ
ボタンをクリックすると、さまざまなバリアントのアラートが表示されます。ライブリージョンコンテナはページ読み込み時からDOMに存在し、コンテンツのみが変更されます。
アクセシビリティ
WAI-ARIA ロール
| ロール | 対象要素 | 説明 |
|---|---|---|
alert | アラートコンテナ | ユーザーのタスクを中断することなく、ユーザーの注意を引く簡潔で重要なメッセージを表示する要素 |
暗黙の ARIA プロパティ
| 属性 | 暗黙の値 | 説明 |
|---|---|---|
aria-live | assertive | スクリーンリーダーを中断して即座にアナウンス |
aria-atomic | true | 変更された部分だけでなく、アラート全体のコンテンツをアナウンス |
キーボードサポート
| キー | アクション |
|---|---|
| Enter | 閉じるボタンをアクティブ化(存在する場合) |
| Space | 閉じるボタンをアクティブ化(存在する場合) |
- スクリーンリーダーは、ライブリージョン内の DOM の変更を検知してアナウンスします。ライブリージョン自体が動的に追加される場合、一部のスクリーンリーダーではコンテンツが確実にアナウンスされない可能性があります。
フォーカス管理
| イベント | 振る舞い |
|---|---|
| アラートはフォーカスを移動してはいけません | アラートは非モーダルであり、フォーカスを奪うことでユーザーのワークフローを中断してはいけません |
| アラートコンテナはフォーカス不可 | アラート要素は tabindex を持たず、キーボードフォーカスを受け取ってはいけません |
| 閉じるボタンはフォーカス可能 | 存在する場合、閉じるボタンは Tab ナビゲーションで到達可能です |
実装ノート
<!-- 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 を使用する場合
- メッセージが情報提供のみでユーザーアクションを必要としない
- ユーザーのワークフローを中断すべきでない
- フォーカスは現在のタスクに留まるべき
Alert Dialog (role=“alertdialog”) を使用する場合
- メッセージが即座のユーザー応答を必要とする
- ユーザーが続行する前に確認またはアクションをとる必要がある
- フォーカスをダイアログに移動すべき(モーダル動作)
重要な注意事項
- ライブリージョンのコンテナ(role=“alert”)は、ページ読み込み時から DOM に存在している必要があります。コンテナ自体を動的に追加・削除しないでください。コンテナ内のコンテンツのみを動的に変更するようにしてください。
参考資料
ソースコード
Alert.vue
<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 rounded-lg border px-4 py-3',
'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 flex-1 items-start gap-3', !hasContent && 'contents')"
>
<template v-if="hasContent">
<span class="apg-alert-icon mt-0.5 flex-shrink-0" 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',
'-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"
@click="handleDismiss"
>
<X class="size-5" aria-hidden="true" />
</button>
</div>
</template> 使い方
Example
<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
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
message | string | - | アラートメッセージの内容 |
variant | 'info' | 'success' | 'warning' | 'error' | 'info' | 視覚的なスタイルバリアント |
dismissible | boolean | false | 閉じるボタンを表示 |
id | string | auto-generated | カスタム ID |
class | string | - | 追加の CSS クラス |
スロット
| スロット | デフォルト | 説明 |
|---|---|---|
default | - | 複雑なコンテンツ(message プロパティの代替) |
Custom Events
| イベント | Detail | 説明 |
|---|---|---|
@dismiss | - | 閉じるボタンがクリックされたときに発行 |
テスト
テストは、ライブリージョンの動作、ARIA属性、アクセシビリティ要件全体にわたってAPG準拠を検証します。Alertコンポーネントは2層のテスト戦略を採用しています。
テスト戦略
ユニットテスト(Testing Library)
フレームワーク固有のテストライブラリを使用してコンポーネントのレンダリング出力を検証します。これらのテストは正しいHTML構造とARIA属性を確認します。
- ARIA 属性 (role="alert")
- ライブリージョンコンテナの DOM 内での永続性
- 閉じるボタンのアクセシビリティ
- jest-axe によるアクセシビリティ検証
E2E テスト (Playwright)
すべてのフレームワークで実際のブラウザ環境でコンポーネントの動作を検証します。これらのテストはインタラクションとフレームワーク間の一貫性をカバーします。
- ライブブラウザでの ARIA 構造
- フォーカス管理(アラートはフォーカスを奪わない)
- 閉じるボタンのキーボード操作
- Tab ナビゲーションの動作
- axe-core アクセシビリティスキャン
- フレームワーク間の一貫性チェック
テストカテゴリ
高優先度 : APG コア準拠(Unit + E2E)
| テスト | APG 要件 |
|---|---|
| role="alert" exists | アラートコンテナは alert ロールを持つ必要がある |
| Container always in DOM | ライブリージョンは動的に追加・削除してはならない |
| Same container on message change | 更新時にコンテナ要素の同一性が保持される |
| Focus unchanged after alert | アラートはキーボードフォーカスを移動してはならない |
| Alert not focusable | アラートコンテナは tabindex を持ってはならない |
中優先度 : アクセシビリティ検証(Unit + E2E)
| テスト | WCAG 要件 |
|---|---|
| No axe violations (with message) | WCAG 2.1 AA 準拠 |
| No axe violations (empty) | WCAG 2.1 AA 準拠 |
| No axe violations (dismissible) | WCAG 2.1 AA 準拠 |
| Dismiss button accessible name | ボタンは aria-label を持つ |
| Dismiss button type="button" | フォーム送信を防ぐ |
低優先度 : Props と拡張性(Unit)
| テスト | 機能 |
|---|---|
| variant prop changes styling | ビジュアルのカスタマイズ |
| id prop sets custom ID | SSR サポート |
| className inheritance | スタイルのカスタマイズ |
| children for complex content | コンテンツの柔軟性 |
| onDismiss callback fires | イベント処理 |
低優先度 : フレームワーク間の一貫性(E2E)
| テスト | 機能 |
|---|---|
| All frameworks have alert | React、Vue、Svelte、Astro すべてがアラート要素をレンダリング |
| Same trigger buttons | すべてのフレームワークで一貫したトリガーボタン |
| Show alert on click | すべてのフレームワークでボタンクリック時にアラートを表示 |
スクリーンリーダーテスト
自動テストは DOM 構造を検証しますが、実際のアナウンス動作を検証するにはスクリーンリーダーによる手動テストが不可欠です。
| スクリーンリーダー | プラットフォーム |
|---|---|
| VoiceOver | macOS / iOS |
| NVDA | Windows |
| JAWS | Windows |
| TalkBack | Android |
メッセージの変更が即座のアナウンスをトリガーすること、およびページ読み込み時に存在するコンテンツはアナウンスされないことを確認してください。
テストツール
- Vitest (opens in new tab) - ユニットテスト用テストランナー
- Testing Library (opens in new tab) - フレームワーク固有のテストユーティリティ(React、Vue、Svelte)
- Playwright (opens in new tab) - E2E テスト用ブラウザ自動化
- axe-core/playwright (opens in new tab) - E2E での自動アクセシビリティテスト
テストの実行
# すべての Alert テストを実行
npm run test -- alert
# 特定のフレームワークのテストを実行
npm run test -- Alert.test.tsx # React
npm run test -- Alert.test.vue # Vue
npm run test -- Alert.test.svelte # Svelte 詳細なドキュメントについては、 testing-strategy.md (opens in new tab) を参照してください。
リソース
- WAI-ARIA APG: Alert パターン (opens in new tab)
- WAI-ARIA APG: Alert Dialog パターン (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist