mcpField
S
stitch-a11y

stitch-a11y

github.com/gabelulテスト・品質4740 回インストール14 週間前に更新
リポジトリ

stitch-a11y について

Stitch のデザインから生成された動作中のコンポーネントを監査・修正するアクセシビリティスキルです。WCAG 2.1 AA に沿って、セマンティクス、キーボード操作、フォーカス、メディア、コントラストを確認し、ソースとレポートを作成します。

主な機能

  1. 016 分野のアクセシビリティ監査
  2. 02セマンティック HTML と ARIA の修正
  3. 03キーボード操作とフォーカス管理の改善
  4. 04画像、メディア、色のコントラスト確認
  5. 05スキップリンク追加と監査レポート作成

活用シーン

  1. 01リリース前の生成コンポーネント確認
  2. 02モーダルやドロップダウンのキー操作修正
  3. 03ページのスクリーンリーダー対応監査

インストール

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

$npx skills add https://github.com/gabelul/stitch-kit --skill stitch-a11y

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

同梱ファイルresources/audit-checklist.md

詳細

Stitch アクセシビリティ監査と修正

あなたはアクセシビリティエンジニアです。Stitch のデザインから生成されたコンポーネントを監査し、WCAG 2.1 AA 違反を特定して、ソースファイルに直接修正を適用します。問題を報告するだけでなく、修正します。

**このスキルは、**コンポーネントの生成後に実行してください。監査を始める前に、コンポーネントが動作している必要があります。

このスキルを使うタイミング

次のような場合にこのスキルを使用してください。

  • コンポーネントの生成と動作が完了しており、リリース前にアクセシビリティのレビューが必要な場合
  • デザインに複雑なインタラクティブパターン(モーダル、ドロップダウン、タブパネル、アコーディオン、カルーセル)が含まれている場合
  • ユーザーが「アクセシビリティ」、「a11y」、「WCAG」、「スクリーンリーダー」、「キーボード操作」に言及した場合
  • 本番環境へのリリースまたはアクセシビリティ監査の準備をしている場合

ステップ 1:監査対象のコンポーネントを特定する

プロジェクトのファイル構造を読み、すべてのコンポーネントファイルを探します。

# Next.js / React
find src -name "*.tsx" -not -path "*/node_modules/*"

# SvelteKit
find src -name "*.svelte" -not -path "*/node_modules/*"

監査の前に、各コンポーネントファイルを読みます。インタラクティブなコンポーネントに注力してください。静的コンテンツよりも、フォーム、ナビゲーション、モーダル、ドロップダウンのほうが注意が必要です。

ステップ 2:監査 — 6 つのカテゴリ

各コンポーネントについて、各カテゴリを順に確認します。

カテゴリ 1:セマンティック HTML

確認する違反:

  • ナビゲーション、ヘッダー、フッター、メインコンテンツ、記事、セクションに <div> または <span> を使用している
  • <button> または <a> ではなく、<div onClick> を使用している
  • 見出しの階層が順序どおりでない(h2 より前に h3 がある、レベルを飛ばしている)
  • レイアウトにテーブルを使用している(データではない)
  • リストをプレーンな <div> 要素としてレンダリングしている

修正:

// ❌ Wrong
<div className="nav">
  <div onClick={goHome}>Home</div>
</div>

// ✅ Fixed
<nav aria-label="Main navigation">
  <a href="/">Home</a>
</nav>

// ❌ Wrong — div button
<div className="btn" onClick={handleClick}>Submit</div>

// ✅ Fixed — real button
<button type="button" onClick={handleClick}>Submit</button>

// ❌ Wrong — visual list as divs
<div className="menu">
  <div>Item 1</div>
  <div>Item 2</div>
</div>

// ✅ Fixed
<ul role="list">
  <li>Item 1</li>
  <li>Item 2</li>
</ul>
カテゴリ 2:ARIA 属性

セマンティック HTML で十分な情報が得られない場合に限り、ARIA を追加してください。不適切な ARIA より、ARIA を使わないほうが適切です。

確認する違反:

  • アクセシブルな名前がない、アイコンのみのボタン
  • aria-label のない <nav> ランドマークが複数ある
  • <main> 要素が複数ある
  • 動的に更新されるのに aria-live がないステータス領域/ライブ領域
  • aria-expanded、aria-haspopup、aria-controls がないインタラクティブ要素

修正:

// Icon-only button
<button aria-label="Close dialog" type="button">
  <XIcon aria-hidden="true" />
</button>

// Multiple nav regions
<nav aria-label="Main navigation">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<nav aria-label="Pagination">...</nav>

// Dropdown toggle
<button
  aria-expanded={isOpen}
  aria-haspopup="menu"
  aria-controls="user-menu"
>
  Account
</button>
<ul id="user-menu" role="menu" hidden={!isOpen}>
  <li role="menuitem"><a href="/profile">Profile</a></li>
</ul>

// Live status region
<div aria-live="polite" aria-atomic="true" className="sr-only">
  {statusMessage}
</div>
カテゴリ 3:キーボード操作

すべてのインタラクティブ要素は、キーボードで操作できなければなりません。次のように考えながら確認してください。ページ内で Tab キーを押していくと、すべての操作に到達して実行できますか?

確認する違反:

  • Tab キーでフォーカスできない、カスタムのインタラクティブ要素
  • フォーカス可能であるべき箇所に tabIndex={-1} を使用している
  • tabIndex={1} 以上を使用している(自然なタブ順序が崩れる)
  • モーダルを開いたときに、フォーカスがモーダル内に移動しない
  • モーダルを閉じたときに、フォーカスがトリガーに戻らない
  • Escape キーでドロップダウンを閉じたときに、フォーカスが戻らない

修正:

// Focus management for modal — React
import { useEffect, useRef } from 'react'

export function Modal({ isOpen, onClose, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null)
  const triggerRef = useRef<HTMLButtonElement>(null)

  useEffect(() => {
    if (isOpen) {
      // Move focus into modal when it opens
      modalRef.current?.focus()
    }
  }, [isOpen])

  function handleClose() {
    onClose()
    // Return focus to trigger when modal closes
    triggerRef.current?.focus()
  }

  return (
    <>
      <button ref={triggerRef} onClick={() => setIsOpen(true)}>
        Open Modal
      </button>
      {isOpen && (
        <div
          ref={modalRef}
          role="dialog"
          aria-modal="true"
          aria-labelledby="modal-title"
          tabIndex={-1}  /* Makes div focusable without entering tab order */
        >
          <h2 id="modal-title">Modal Title</h2>
          {children}
          <button onClick={handleClose}>Close</button>
        </div>
      )}
    </>
  )
}

// Keyboard handler for custom interactive elements
<div
  role="button"
  tabIndex={0}
  onClick={handleAction}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault()
      handleAction()
    }
  }}
>
  Custom button behavior
</div>

<script lang="ts">
  let dialogEl = $state<HTMLDialogElement>()
  let triggerEl = $state<HTMLButtonElement>()
  let isOpen = $state(false)

  function openDialog() {
    isOpen = true
    // tick() ensures DOM is updated before focusing
    tick().then(() => dialogEl?.focus())
  }

  function closeDialog() {
    isOpen = false
    triggerEl?.focus()  // Return focus to trigger
  }
</script>

<button bind:this={triggerEl} onclick={openDialog}>Open</button>

{#if isOpen}
  <dialog
    bind:this={dialogEl}
    tabindex="-1"
    aria-modal="true"
    onkeydown={(e) => e.key === 'Escape' && closeDialog()}
  >
    <button onclick={closeDialog}>Close</button>
  </dialog>
{/if}
カテゴリ 4:フォーカスの視認性

すべてのインタラクティブ要素には、視認できるフォーカスインジケーターが必要です。同等に視認できる代替スタイルを用意せずに、フォーカスリングを削除しないでください。

確認する違反:

  • カスタムのフォーカススタイルがないのに outline: none または outline: 0 を使用している
  • focus-visible:ring-* を指定せずに、Tailwind の .focus:outline-none を使用している
  • キーボードフォーカス時ではなく、クリック時にのみ表示されるフォーカススタイル

修正:

CSS の場合:

/* Never this */
*:focus { outline: none; }

/* Always this — uses :focus-visible to show only on keyboard focus */
*:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
  border-radius: 2px;
}

Tailwind の場合:

// ❌ Wrong
<button className="focus:outline-none">

// ✅ Fixed
<button className="focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2">
カテゴリ 5:画像とメディア

確認する違反:

  • alt 属性がない <img> または <Image>
  • 意味のある画像に alt="" を指定している
  • スクリーンリーダーに不要な情報を加える、説明的な代替テキストが付いた装飾画像
  • インタラクティブ要素として使われているのに、アクセシブルなラベルがないアイコン
  • キャプションがない動画

修正:

// Meaningful image
<Image src="/hero.jpg" alt="Team members collaborating at a whiteboard in a modern office" />

// Decorative image — empty alt so screen readers skip it
<Image src="/bg-pattern.svg" alt="" aria-hidden="true" />

// Icon in a button — hide icon, label the button
<button aria-label="Delete item">
  <TrashIcon aria-hidden="true" />
</button>

// Icon with adjacent text — hide the icon (it's redundant)
<button>
  <SaveIcon aria-hidden="true" />
  <span>Save changes</span>
</button>
カテゴリ 6:色とコントラスト

自動化ツールを使わず、デザインから判断して次の点を確認します。

確認する違反:

  • ミュートされた背景(--color-surface)上のミュートテキスト(--color-text-muted)— 4.5:1 を満たさないことがよくあります
  • 白地に小さいサイズで表示したプライマリカラー — コントラスト比 4.5:1 を満たしているか確認する
  • ヒントとしても読みづらいほど薄い、無効状態のテキスト
  • 意味を伝えるために色のみに依存している(エラー状態、必須フィールド)

修正:

// Add non-color indicator for errors
<input
  aria-invalid={hasError}
  aria-describedby={hasError ? 'email-error' : undefined}
  className={hasError ? 'border-error' : 'border-border'}
/>
{hasError && (
  <p id="email-error" role="alert" className="text-error">
    {/* Icon + text — not color alone */}
    <AlertIcon aria-hidden="true" />
    Please enter a valid email address
  </p>
)}

// Required field indicator
<label>
  Email
  <span aria-hidden="true" className="text-error"> *</span>
  <span className="sr-only">(required)</span>
</label>

ステップ 3:sr-only ユーティリティクラス

まだ存在しない場合は、このクラスをグローバル CSS に追加してください。頻繁に使用します。

/* Visually hidden, but readable by screen readers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* Skip link — visible on focus for keyboard users */
.skip-link {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
.skip-link:focus {
  position: fixed;
  top: 1rem;
  left: 1rem;
  width: auto;
  height: auto;
  padding: 0.5rem 1rem;
  background: var(--color-primary);
  color: var(--color-primary-fg);
  border-radius: var(--radius-md);
  font-weight: 600;
  z-index: 9999;
}

ステップ 4:ナビゲーションをスキップするリンク

すべてのページレイアウトで、最初の要素としてスキップリンクを追加します。これにより、キーボードユーザーはナビゲーションを飛ばして移動できます。

// app/layout.tsx or +layout.svelte — first child of <body>
<a href="#main-content" className="skip-link">
  Skip to main content
</a>

// The target
<main id="main-content" tabIndex={-1}>
  {children}
</main>

ステップ 5:監査レポートを作成する

修正後、検出した問題と修正内容をまとめた accessibility-audit.md を作成します。

# Accessibility Audit Report

**Date:** [date]
**WCAG Target:** 2.1 AA
**Components audited:** [list]

## Issues Found & Fixed

### Critical (would block screen reader users)
- [Component]: [issue] → [fix applied]

### Important (keyboard navigation issues)
- [Component]: [issue] → [fix applied]

### Minor (improvements to quality of life)
- [Component]: [issue] → [fix applied]

## Remaining Recommendations

[Any issues that require design changes or user testing to resolve]

## How to test

1. Tab through the entire page — every interactive element should be reachable
2. Activate with Enter/Space — all buttons and links should work
3. Test with VoiceOver (Mac) or NVDA (Windows) — key flows should be narrated correctly
4. Browser DevTools → Rendering → Emulate prefers-reduced-motion → Verify animations stop
5. axe DevTools extension for automated checks

トラブルシューティング

問題修正方法
aria-labelledby が誤った ID を参照しているページ全体で ID が一意になるようにします
フォーカストラップによってキーボード操作がモーダル内に閉じ込められるモーダルの範囲内で適切に Tab/Shift+Tab を循環させます
スクリーンリーダーが冗長な情報を読み上げる装飾要素に aria-hidden="true" を追加します
1 つのコンポーネントに複数の違反があるまずセマンティック HTML を修正します。ARIA の問題は、多くの場合そこから派生しています
スキップリンクが表示されない:focus の状態で画面外に配置するスタイルが上書きされるようにします

参考資料

  • resources/audit-checklist.md — 出荷前レビュー用の簡易チェックリスト

コメント