APG Patterns
English
English

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

プロパティデフォルト説明
messagestring-アラートメッセージの内容
variant'info' | 'success' | 'warning' | 'error''info'視覚的なスタイルバリアント
dismissiblebooleanfalse閉じるボタンを表示
idstringauto-generatedカスタム ID
classstring-追加の 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 IDSSR サポート
className inheritanceスタイルのカスタマイズ
children for complex contentコンテンツの柔軟性
onDismiss callback firesイベント処理

低優先度: フレームワーク間の一貫性(E2E)

テスト機能
All frameworks have alertReact、Vue、Svelte、Astro すべてがアラート要素をレンダリング
Same trigger buttonsすべてのフレームワークで一貫したトリガーボタン
Show alert on clickすべてのフレームワークでボタンクリック時にアラートを表示

スクリーンリーダーテスト

自動テストは DOM 構造を検証しますが、実際のアナウンス動作を検証するにはスクリーンリーダーによる手動テストが不可欠です。

スクリーンリーダープラットフォーム
VoiceOvermacOS / iOS
NVDAWindows
JAWSWindows
TalkBackAndroid

メッセージの変更が即座のアナウンスをトリガーすること、およびページ読み込み時に存在するコンテンツはアナウンスされないことを確認してください。

テストツール

テストの実行

# すべての 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

詳細はテスト戦略ガイドを参照してください。

リソース