mcpField
W
web-accessibility

web-accessibility

github.com/hoodiniデザイン・UI291455 回インストール1 日前に更新
リポジトリ

web-accessibility について

Web 開発者向けに、WCAG 2.1 に沿ったアクセシブルなアプリの構築方法をまとめたスキルです。ARIA パターン、コントラスト要件、フォーム例を扱います。

主な機能

  1. 01操作要素とナビゲーションの ARIA パターン
  2. 02WCAG AA のコントラスト比
  3. 03アクセシブルなフォーム例

活用シーン

  1. 01モーダルダイアログやメニューの実装
  2. 02テキストと UI の色コントラストの確認
  3. 03必須項目を示すフォームの設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/hoodini/ai-agents-skills --skill web-accessibility

インストール後、Claude Code で/web-accessibilityを入力して読み込まれたか確認します。

詳細

ウェブアクセシビリティ(WCAG 2.1)

誰もが利用できる、アクセシブルなウェブアプリケーションを構築します。

ARIA パターン

ボタン
<button
  type="button"
  aria-pressed={isPressed}
  aria-disabled={isDisabled}
  onClick={handleClick}
>
  Toggle Feature
</button>
モーダルダイアログ
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
>
  <h2 id="modal-title">Confirm Action</h2>
  <p id="modal-description">Are you sure you want to proceed?</p>
  <button onClick={onConfirm}>Confirm</button>
  <button onClick={onCancel}>Cancel</button>
</div>
ナビゲーションメニュー
<nav aria-label="Main navigation">
  <ul role="menubar">
    <li role="none">
      <a role="menuitem" href="/home">Home</a>
    </li>
    <li role="none">
      <button
        role="menuitem"
        aria-haspopup="true"
        aria-expanded={isOpen}
      >
        Products
      </button>
      {isOpen && (
        <ul role="menu" aria-label="Products submenu">
          <li role="none">
            <a role="menuitem" href="/products/new">New</a>
          </li>
        </ul>
      )}
    </li>
  </ul>
</nav>

キーボード操作

フォーカス管理
import { useEffect, useRef } from 'react';

function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocus = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      previousFocus.current = document.activeElement as HTMLElement;
      modalRef.current?.focus();
    } else {
      previousFocus.current?.focus();
    }
  }, [isOpen]);

  // Trap focus within modal
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Escape') {
      onClose();
    }
    
    if (e.key === 'Tab') {
      const focusable = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      
      if (focusable && focusable.length > 0) {
        const first = focusable[0] as HTMLElement;
        const last = focusable[focusable.length - 1] as HTMLElement;
        
        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      }
    }
  };

  if (!isOpen) return null;

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      tabIndex={-1}
      onKeyDown={handleKeyDown}
    >
      {children}
    </div>
  );
}

色のコントラスト

最小コントラスト比(WCAG AA):

  • 通常のテキスト:4.5:1
  • 大きなテキスト(18pt 以上):3:1
  • UI コンポーネント:3:1
function getContrastRatio(color1: string, color2: string): number {
  const lum1 = getLuminance(color1);
  const lum2 = getLuminance(color2);
  const lighter = Math.max(lum1, lum2);
  const darker = Math.min(lum1, lum2);
  return (lighter + 0.05) / (darker + 0.05);
}

function getLuminance(hex: string): number {
  const rgb = hexToRgb(hex);
  const [r, g, b] = rgb.map((c) => {
    c = c / 255;
    return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

アクセシブルなフォーム

<form onSubmit={handleSubmit}>
  <div>
    <label htmlFor="email">
      Email address
      <span aria-hidden="true">*</span>
      <span className="sr-only">(required)</span>
    </label>
    <input
      id="email"
      type="email"
      aria-required="true"
      aria-invalid={errors.email ? 'true' : 'false'}
      aria-describedby={errors.email ? 'email-error' : undefined}
    />
    {errors.email && (
      <p id="email-error" role="alert" className="error">
        {errors.email}
      </p>
    )}
  </div>
  
  <button type="submit">Submit</button>
</form>

スクリーンリーダー専用コンテンツ

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

テスト

# Automated testing
npm install -D axe-core @axe-core/react

# In tests
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('component is accessible', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

リソース

コメント