stitch-a11y について
Stitch のデザインから生成された動作中のコンポーネントを監査・修正するアクセシビリティスキルです。WCAG 2.1 AA に沿って、セマンティクス、キーボード操作、フォーカス、メディア、コントラストを確認し、ソースとレポートを作成します。
主な機能
- 016 分野のアクセシビリティ監査
- 02セマンティック HTML と ARIA の修正
- 03キーボード操作とフォーカス管理の改善
- 04画像、メディア、色のコントラスト確認
- 05スキップリンク追加と監査レポート作成
活用シーン
- 01リリース前の生成コンポーネント確認
- 02モーダルやドロップダウンのキー操作修正
- 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— 出荷前レビュー用の簡易チェックリスト