APG Patterns
English
English

Alert

ユーザーのタスクを中断せずに、重要なメッセージを目立つ形で表示する要素。

デモ

ボタンをクリックすると、さまざまなバリアントのアラートが表示されます。ライブリージョンコンテナはページ読み込み時からDOMに存在し、コンテンツのみが変更されます。

デモのみ表示 →

アクセシビリティ

WAI-ARIA ロール

ロール対象要素説明
alertアラートコンテナユーザーのタスクを中断することなく、ユーザーの注意を引く簡潔で重要なメッセージを表示する要素

暗黙の ARIA プロパティ

属性暗黙の値説明
aria-liveassertiveスクリーンリーダーを中断して即座にアナウンス
aria-atomictrue変更された部分だけでなく、アラート全体のコンテンツをアナウンス

キーボードサポート

キーアクション
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

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

テストツール

テストの実行

# すべての 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) を参照してください。

リソース