prefers-reduced-motion対応をClaude Codeで進める洗い出しと検査
動きを減らす設定に自分のサイトが従っているかを、Claude Codeに洗い出させて直し、Playwrightのreducedmotionエミュレーションで検査する手順です。
prefers-reduced-motionへの対応は、「動きを減らしたい」という利用者の設定を読み、アニメーションを弱めるか置き換える作業です。実装の本体は数行のCSSですが、難しいのは洗い出しと検査です。どのファイルにどんな動きが散らばっているかを人が数えるのは手間がかかります。
この2つはClaude Codeに向いた仕事です。コードベース全体を検索して候補を挙げる作業と、Playwrightの検査を回して結果から直す往復を任せます。この記事は、洗い出し、CSSとJavaScriptでの止め方、emulateMediaによる検査、そして編集後に自動で検査を回すhookまでを順に組みます。
prefers-reduced-motionは何を伝える設定か
prefers-reduced-motionは、利用者が端末で「不要な動きを最小限にしたい」と設定したかを知らせるCSSのメディア特性です。値は no-preference と reduce の2つです。reduce は設定が有効なときに真になり、@media (prefers-reduced-motion) と書いても reduce を指定したのと同じに扱われます。
この設定が必要な理由は身体的なものです。大きな要素の拡大やパン(平行移動)は、前庭覚に障害のある人に不快感を起こすことがあります。MDNはスケーリングやパンを前庭運動のトリガーの例に挙げています。だから対応の目標は、動きを消すことではなく、トリガーになりうる動きを穏やかなものに置き換えることです。
利用者側の設定場所はOSごとに違います。確認用に控えておくと、あとで実機を見るときに役立ちます。
- macOS(Sequoiaまで): System Settings > Accessibility > Display > Reduce motion
- macOS Tahoe以降: System Settings > Accessibility > Motion > Reduce motion
- Windows 11: Settings > Accessibility > Visual Effects > Animation Effects
- iOS: Settings > Accessibility > Motion
- Android 9以降: Settings > Accessibility > Remove animations
Firefoxでは about:config に数値の ui.prefersReducedMotion を作り、1 にすると削減の希望を示せます。0 で通常の動きに戻ります。
Claude Codeに動きの一覧を作らせる
最初にやるのは、サイトのどこが動くかの棚卸しです。動きは見つけにくい場所にもあります。CSSの animation や transition に加えて、スクロール連動、JavaScriptによるスクロール、自動再生の動画やカルーセルが該当します。
Claude Codeに渡す指示は、探す対象を列挙して、結果を表で返させる形が安定します。
このリポジトリで、ユーザーの操作なしに動く、または大きく動く表現を洗い出して。
対象: CSSのanimation / transition / @keyframes、scroll-behavior、
JSのscrollTo({behavior:"smooth"})、requestAnimationFrame、setInterval、
autoplayのvideo、アニメーション系ライブラリの呼び出し。
各件を「ファイル:行 / 種別 / 動きの内容 / 無限に繰り返すか」の表で出して。
まだ修正はしないで。検索そのものは rg(ripgrep)で足ります。Claude Codeに任せるときも、候補の網を広くかけてから人が読んで絞る流れは同じです。手元で先に当たりを付けるなら次のようにします。
rg -n "@keyframes|animation\s*:|animation-name|transition\s*:" \
--glob "*.{css,scss,tsx,vue,svelte}" src
rg -n "scroll-behavior|behavior:\s*['\"]smooth|requestAnimationFrame|autoplay" src洗い出しの対象と、見落としやすい点を表にまとめます。
| 種別 | 探す手がかり | 見落としやすい点 |
|---|---|---|
| CSSアニメーション | 探す手がかり@keyframes、animation | 見落としやすい点無限に繰り返すもの(infinite)は最優先 |
| CSSトランジション | 探す手がかりtransition | 見落としやすい点ホバーで大きく動くスケールや移動 |
| スクロールの動き | 探す手がかりscroll-behavior: smooth | 見落としやすい点アンカーリンクの移動にも効く |
| JavaScript | 探す手がかりscrollTo、requestAnimationFrame | 見落としやすい点CSSの設定が届かない |
| メディア | 探す手がかりautoplay、loop | 見落としやすい点動画やGIFはCSSでは止まらない |
返ってきた表は、そのまま作業リストになります。「無限に繰り返す」列が真の行から直すと、効果が大きい順に進みます。
CSSで止める — 消すのではなく穏やかなものに替える
CSSでの基本形は、通常のアニメーションを書いたあとに、reduce のメディアクエリで上書きする形です。MDNの例も、同じ詳細度のルールを後ろに置いて上書きし、拡大縮小のpulseを不透明度だけが変わるdissolveに置き換えています。
.animation {
animation: pulse 1s linear infinite both;
}
@media (prefers-reduced-motion: reduce) {
.animation {
animation: dissolve 4s linear infinite both;
}
}ここで押さえたいのは、置き換えた後も、変化がゼロにはなっていないことです。拡大縮小という前庭運動のトリガーが外れ、不透明度の緩やかな変化だけが残ります。状態の変化を伝える役目のあるアニメーション、たとえば読み込み中を示すスピナーは、完全に止めると情報が失われます。回転を点滅やフェードに替えるのが無難です。
サイト全体に一括で効かせるルールも書けますが、副作用を点検してから入れます。
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}この種の一括指定は、animationend や transitionend を待って次の処理へ進むコードがあると、挙動が変わることがあります。Claude Codeに依頼するときは、「一括指定を入れる前に、animationend / transitionend を購読しているコードを列挙して」と先に頼みます。購読があるなら、個別の置き換えに切り替える材料になります。
JavaScriptで動かす部分はmatchMediaで分岐する
CSSで止められないのは、JavaScriptが起こす動きです。スムーズスクロール、requestAnimationFrame で描くアニメーション、ライブラリが回す演出がここに入ります。この種のコードは matchMedia で設定を読んで分岐します。
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
export function scrollToSection(el: HTMLElement) {
el.scrollIntoView({ behavior: query.matches ? "auto" : "smooth" });
}
query.addEventListener("change", (e) => {
// 利用中に設定を切り替えた場合も追従する
document.documentElement.dataset.motion = e.matches ? "reduce" : "full";
});設定は、ページを開いたあとに利用者がOSで切り替えることもあります。change イベントを購読しておけば、再読み込みなしで追従できます。Claude Codeには「matchMedia を毎回呼ぶ形を、1か所のユーティリティにまとめて」と頼むと、分岐の置き場が散らばりません。
自動再生の動画は、CSSでは止まりません。reduce のときは autoplay を付けない、または再生ボタンを出して利用者に委ねる構成に変えます。
PlaywrightのemulateMediaでreduceを再現して検査する
直したつもりでも、検査がなければ崩れていきます。PlaywrightはブラウザのOS設定を触らずに、prefers-reduced-motion を再現できます。page.emulateMedia() の reducedMotion に 'reduce' か 'no-preference' を渡します。null を渡すとエミュレーションを無効にします。
テスト全体に効かせるなら、test.use か設定ファイルの use に reducedMotion を書きます。このオプションは 'reduce' と 'no-preference' を受け取り、既定値は 'no-preference' です。
検査の方針は2つの面から挟みます。reduce では無限に繰り返す動きが残っていないことを確かめます。no-preference では動きが残っていることを確かめ、検査が空振りでないことを保証します。
// tests/a11y/reduced-motion.spec.ts
import { test, expect, type Page } from "@playwright/test";
const paths = ["/", "/pricing"];
async function infiniteAnimations(page: Page) {
return page.evaluate(() =>
[...document.querySelectorAll("*")]
.filter((el) => {
const s = getComputedStyle(el);
return (
s.animationName !== "none" &&
s.animationIterationCount.includes("infinite")
);
})
.map((el) => el.tagName.toLowerCase() + "." + el.className),
);
}
for (const path of paths) {
test(`${path}: reduce で無限アニメーションが残らない`, async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto(path);
expect(await infiniteAnimations(page)).toEqual([]);
});
test(`${path}: no-preference では動きが生きている`, async ({ page }) => {
await page.emulateMedia({ reducedMotion: "no-preference" });
await page.goto(path);
// 動きのあるページだけを paths に入れる前提
expect((await infiniteAnimations(page)).length).toBeGreaterThan(0);
});
}後者のテストは、洗い出しの表で動きがあると分かったページだけに当てます。動きのないページでは、reduce の検査は何があっても通ってしまいます。通常側の断言が、検査の有効性を支えます。
実行は次のとおりです。
npx playwright test tests/a11y/reduced-motion.spec.ts --reporter=line失敗時に出る要素名を、そのままClaude Codeに読ませます。「div.hero-glow が reduce でも無限に動いている」と分かれば、該当CSSを直す指示は短く済みます。
画面の崩れを画像で比べる使い方もあります。reduce と通常でスクリーンショットを撮り、差が想定の範囲かを見る流れは、Playwrightのスクリーンショット比較の記事が扱っています。動きの有無そのものはコンピューテッドスタイルで機械的に判定でき、画像比較より安定します。
CLAUDE.mdとStop hookで検査を回し続ける
洗い出しと修正は一度やれば終わりではなく、新しいコンポーネントが増えるたびに崩れます。そこで、規約をCLAUDE.mdに書き、編集の区切りで検査を自動で回します。
CLAUDE.mdには、Claude Codeが新規のUIを書くときに守る最小限の規約を置きます。
## モーション
- 新しい animation / transition を足すときは、同じ変更で
`@media (prefers-reduced-motion: reduce)` の置き換えも書く。
- `infinite` のアニメーションは reduce で必ず止めるか、不透明度だけの変化にする。
- スクロールやrAFを使うJSは、`matchMedia("(prefers-reduced-motion: reduce)")` で分岐する。
- 検査: `npx playwright test tests/a11y/reduced-motion.spec.ts`次に、Claude Codeが応答を終えるたびに検査を走らせるStop hookを置きます。Stopフックが終了コード2で終わると、Claude Codeは停止せず会話を続けます。stderrに書いた内容が、Claudeに渡る理由になります。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": ".claude/hooks/motion-check.sh",
"timeout": 180
}
]
}
]
}
}#!/bin/bash
# .claude/hooks/motion-check.sh
input=$(cat)
# stop hook で続行中のときは再検査しない(無限ループ防止)
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
out=$(npx playwright test tests/a11y/reduced-motion.spec.ts --reporter=line 2>&1)
if [ $? -ne 0 ]; then
echo "prefers-reduced-motion の検査が失敗しました。" >&2
echo "$out" | tail -n 30 >&2
exit 2
fi
exit 0stop_hook_active は、すでにStopフックの結果で会話が続いているときに true になる入力項目です。ここを見ずに毎回ブロックすると、直らない失敗でループします。Claude Code側にも、Stopフックが連続8回続行させると次のブロックを無視して終える上限があります。上限は環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変えられます。
Stopのたびに数分かかる検査は重い、と感じる場合は選択肢が2つあります。1つは、CSSやコンポーネントを触った回だけ走らせる条件を、スクリプトの先頭で git diff --name-only から判定すること。もう1つは、編集のたびに走る PostToolUse で、小さな静的チェックだけを回すことです。後者の組み立てはPostToolUse hookのLint自動修正の記事が詳しく扱っています。PostToolUse は終了コード2を返しても、すでに実行されたツールは止まらず、stderrがClaudeに見えるだけです。
検査が通ったのに動きが残るときの切り分け
Playwrightの検査が緑でも、実機で動きが残ることがあります。切り分けは、動きの種類で分けると早く済みます。
CSSが効いていない場合。上書きのルールが、元のルールより前に書かれていると勝てません。MDNの例も「同じ詳細度で、後ろに置く」ことで成り立っています。ビルド後のCSSで順序が入れ替わっていないかを確認します。
JavaScript由来の場合。コンピューテッドスタイルに現れない動きは、getComputedStyle の検査をすり抜けます。requestAnimationFrame で描く動きや、ライブラリが直接 style を書き換える動きです。この種は、ページ内でメディアクエリの結果を読む検査を足します。
test("reduce が JS に届いている", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/");
const matched = await page.evaluate(
() => matchMedia("(prefers-reduced-motion: reduce)").matches,
);
expect(matched).toBe(true);
});自動再生メディアの場合。 video の autoplay は、コンピューテッドスタイルの検査では見えません。page.locator("video[autoplay]").count() が0であることを、reduce の下で断言します。
エミュレーションと実機の差。 emulateMedia はブラウザのメディア特性だけを書き換えます。OSの設定が引き金になるほかの挙動、たとえばブラウザ組み込みのスムーズスクロールの判断などは、実機の設定で最終確認します。前節のOS別の設定場所から、手元の端末で一度は動きを見ておきます。
アクセシビリティ検査の全体像は別の観点です。動きの削減と、コントラストやラベルのような静的な違反は、検査の仕組みを共通にして並べると保守が楽になります。axeの検査をPRで落とす構成はPlaywrightとaxeのCI記事にあります。
まとめ
動きの削減は、Claude Codeに洗い出しを任せ、CSSとJavaScriptの両面で穏やかな表現に置き換え、emulateMedia の reduce で検査すれば回ります。最後に残る判断は、「どの動きが情報を運んでいるか」で、消してよいものと置き換えるものを分けることです。ここは表の「動きの内容」列を読みながら、人が決めるところです。検査はその判断を後から守る役で、新しい動きが足されたときの見張りとして働きます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。