APG Patterns
English GitHub
English GitHub

Switch

ユーザーが2つの状態(オンとオフ)を切り替えることができるコントロール。

🤖 AI Implementation Guide

デモ

デモのみ表示 →

アクセシビリティ

WAI-ARIA ロール

WAI-ARIA ステート

aria-checked

スイッチの現在のチェック状態を示します。

true | false
必須 はい(switchロールの場合)
デフォルト initialChecked プロパティ(デフォルト: false
変更トリガー クリック、Enter、Space
リファレンス aria-checked (opens in new tab)

aria-disabled

スイッチが認識可能だが無効化されていることを示します。

true | undefined
必須 いいえ(無効化時のみ)
リファレンス aria-disabled (opens in new tab)

キーボードサポート

キー アクション
Space スイッチの状態を切り替え(オン/オフ)
Enter スイッチの状態を切り替え(オン/オフ)

アクセシブルな名前

スイッチにはアクセシブルな名前が必要です。以下の方法で提供できます:

  • 表示されるラベル(推奨) - スイッチの子要素のコンテンツがアクセシブルな名前を提供
  • aria-label - スイッチに非表示のラベルを提供
  • aria-labelledby - 外部要素をラベルとして参照

ビジュアルデザイン

この実装は、色のみに依存せず状態を示すことでWCAG 1.4.1(色の使用)に準拠しています:

  • つまみの位置 - 左 = オフ、右 = オン
  • チェックマークアイコン - スイッチがオンの時のみ表示
  • 強制カラーモード - Windowsハイコントラストモードでのアクセシビリティのためシステムカラーを使用

ソースコード

Switch.astro
---
/**
 * APG Switch Pattern - Astro Implementation
 *
 * A control that allows users to toggle between two states: on and off.
 * Uses Web Components for client-side interactivity.
 *
 * @see https://www.w3.org/WAI/ARIA/apg/patterns/switch/
 */

export interface Props {
  /** Initial checked state */
  initialChecked?: boolean;
  /** Whether the switch is disabled */
  disabled?: boolean;
  /** Additional CSS class */
  class?: string;
}

const { initialChecked = false, disabled = false, class: className = '' } = Astro.props;
---

<apg-switch class={className}>
  <button
    type="button"
    role="switch"
    class="apg-switch"
    aria-checked={initialChecked}
    aria-disabled={disabled || undefined}
    disabled={disabled}
  >
    <span class="apg-switch-track">
      <span class="apg-switch-icon" aria-hidden="true">
        <svg viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
          <path
            d="M10.28 2.28a.75.75 0 00-1.06-1.06L4.5 5.94 2.78 4.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.06 0l5.25-5.25z"
            fill="currentColor"></path>
        </svg>
      </span>
      <span class="apg-switch-thumb"></span>
    </span>
    {
      Astro.slots.has('default') && (
        <span class="apg-switch-label">
          <slot />
        </span>
      )
    }
  </button>
</apg-switch>

<script>
  class ApgSwitch extends HTMLElement {
    private button: HTMLButtonElement | null = null;
    private rafId: number | null = null;

    connectedCallback() {
      this.rafId = requestAnimationFrame(() => this.initialize());
    }

    private initialize() {
      this.rafId = null;
      this.button = this.querySelector('button[role="switch"]');
      if (!this.button) {
        console.warn('apg-switch: button element not found');
        return;
      }

      this.button.addEventListener('click', this.handleClick);
      this.button.addEventListener('keydown', this.handleKeyDown);
    }

    disconnectedCallback() {
      if (this.rafId !== null) {
        cancelAnimationFrame(this.rafId);
        this.rafId = null;
      }
      this.button?.removeEventListener('click', this.handleClick);
      this.button?.removeEventListener('keydown', this.handleKeyDown);
      this.button = null;
    }

    private toggle() {
      if (!this.button || this.button.disabled) return;

      const currentChecked = this.button.getAttribute('aria-checked') === 'true';
      const newChecked = !currentChecked;

      this.button.setAttribute('aria-checked', String(newChecked));

      this.dispatchEvent(
        new CustomEvent('change', {
          detail: { checked: newChecked },
          bubbles: true,
        })
      );
    }

    private handleClick = () => {
      this.toggle();
    };

    private handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === ' ' || event.key === 'Enter') {
        event.preventDefault();
        this.toggle();
      }
    };
  }

  if (!customElements.get('apg-switch')) {
    customElements.define('apg-switch', ApgSwitch);
  }
</script>

使い方

使用例
---
import Switch from './Switch.astro';
---

<Switch initialChecked={false}>
  Enable notifications
</Switch>

<script>
  // Listen for change events
  document.querySelector('apg-switch')?.addEventListener('change', (e) => {
    console.log('Checked:', e.detail.checked);
  });
</script>

API

プロパティ デフォルト 説明
initialChecked boolean false 初期のチェック状態
disabled boolean false スイッチが無効かどうか
class string "" 追加のCSSクラス

カスタムイベント

イベント 詳細 説明
change { checked: boolean } スイッチの状態が変更されたときに発火

スロット

スロット 説明
default スイッチのラベルコンテンツ

テスト

テストは、キーボードインタラクション、ARIA属性、アクセシビリティ要件全体でAPG準拠を検証します。Switchコンポーネントは2層のテスト戦略を採用しています。

テスト戦略

ユニットテスト(Testing Library)

フレームワーク固有のテストライブラリを使用してコンポーネントのレンダリング出力を検証します。これらのテストは正しいHTML構造とARIA属性を確認します。

  • ARIA属性(role="switch"、aria-checked)
  • キーボードインタラクション(Space、Enter)
  • 無効状態の処理
  • jest-axeによるアクセシビリティ検証

E2Eテスト(Playwright)

すべてのフレームワークで実際のブラウザ環境でコンポーネントの動作を検証します。これらのテストはインタラクションとフレームワーク間の一貫性をカバーします。

  • クリックとキーボードのトグル動作
  • ライブブラウザでのARIA構造
  • 無効状態のインタラクション
  • axe-coreによるアクセシビリティスキャン
  • フレームワーク間の一貫性チェック

テストカテゴリ

高優先度: APG キーボードインタラクション(Unit + E2E)

テスト 説明
Space key スイッチの状態を切り替え
Enter key スイッチの状態を切り替え
Tab navigation Tabキーでスイッチ間をフォーカス移動
Disabled Tab skip 無効化されたスイッチはTab順序でスキップされる
Disabled key ignore 無効化されたスイッチはキー押下を無視する

高優先度: APG ARIA 属性(Unit + E2E)

テスト 説明
role="switch" 要素にswitchロールが設定されている
aria-checked initial 初期状態が aria-checked="false"
aria-checked toggle クリックで aria-checked 値が変更される
type="button" 明示的なボタンタイプでフォーム送信を防止
aria-disabled 無効化されたスイッチは aria-disabled="true" を持つ

中優先度: アクセシビリティ(Unit + E2E)

テスト 説明
axe violations WCAG 2.1 AA違反なし(jest-axe経由)
Accessible name (children) スイッチが子要素のコンテンツから名前を持つ
aria-label aria-label経由でアクセシブルな名前
aria-labelledby 外部要素経由でアクセシブルな名前

低優先度: HTML属性継承(Unit)

テスト 説明
className merge カスタムクラスがコンポーネントクラスとマージされる
data-* attributes カスタムdata属性が引き継がれる

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

テスト 説明
All frameworks have switch React、Vue、Svelte、Astroすべてがスイッチ要素をレンダリング
Toggle on click すべてのフレームワークでクリック時に正しくトグル
Consistent ARIA すべてのフレームワークで一貫したARIA構造

テストツール

完全なドキュメントは testing-strategy.md (opens in new tab) を参照してください。

リソース