accessible-animation について
動きを減らす設定に対応したウェブアニメーションの作成・監査向けスキルです。動きを削除・緩和・維持の3段階に分け、CSSやJavaScriptの実装例とスクリーンショットによる検証方法を示します。
主な機能
- 01動きを減らす3段階の指針
- 02CSSメディアクエリのパターン
- 03JavaScript matchMedia制御
- 04Reactの動きを減らすフック
- 05単体デモの検証
活用シーン
- 01不快感を招くアニメーションの監査
- 02GSAPやFramer Motionの効果の調整
- 03動きを減らす設定に対応したウェブデモの作成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ iart-ai/ web-animation-skills --skill accessible-animationインストール後、Claude Code で/accessible-animationを入力して読み込まれたか確認します。
同梱ファイルREADME.mdreferences/patterns.md
詳細
アクセシブルなアニメーション(動きの軽減、段階的な調整)
prefers-reduced-motion: reduce はキルスイッチではなく、適切な代替手段を備えた段階的な調整として尊重してください。OS レベルの設定(macOS:設定 > アクセシビリティ > ディスプレイ > 視差効果を減らす、Windows:設定 > アクセシビリティ > 視覚効果 > アニメーション効果、iOS/Android の同等の設定)は、前庭を刺激する動きが吐き気、めまい、片頭痛を引き起こすことを示しています。正しい対応は、方向を把握するための手がかりを残しながら、危険な動きを取り除くことです。
使用するタイミング
Web UI にアニメーションを追加または監査するとき、ユーザーから乗り物酔いのような症状や WCAG に関する指摘があったとき、またはアクセシブルな製品にアニメーションライブラリ(GSAP、Framer Motion、Lenis のスムーズスクロール、Anime.js、CSS キーフレーム)を組み込むときに使用します。
基本原則:動きを段階分けし、すべてをなくさない
すべてのアニメーションを取り除くのは、よくある過剰修正です。遷移のない瞬時の状態変化は、かえって混乱を招くこと(要素が突然現れるなど)があり、有用なアフォーダンス(フォーカスリングや、進捗を伝えるローディングスピナー)もなくなります。すべてのアニメーションを次の3段階に分類します。
- 段階1 — 完全に取り除く(前庭を刺激するもの)。 視差効果、ビューポートを横切る広範囲のスライド/移動、大きな要素の拡大・縮小/ズーム、3D 回転やスピン、連続して自動再生するカルーセルやマーキー、スムーズスクロールの乗っ取り(Lenis/Locomotive)、スクロールに連動して固定されるシーン、モーションパスアニメーション、画面の広い範囲を動かすものや奥行き/自己移動を連想させるもの。
- 段階2 — 弱める/短くする。 動きの代わりに、短い不透明度フェード(≤200ms)を使用します。移動距離と再生時間を減らします。遷移そのものは残し(要素が突然現れるのを防ぎ)、位置の移動は取り除きます。左からのスライドイン(600ms)は、短いクロスフェード(150ms)に置き換えます。
- 段階3 — 常に残す。 不透明度フェード、色/背景の遷移、フォーカスリングの遷移、ローディングインジケーター、意味を伝えるために不可欠な動き(WCAG では不可欠なアニメーションが認められています)。これらは自己移動を連想させないため、前庭反応を引き起こすことはほとんどありません。
CSS レイヤー:メディアクエリによる制御
動きをデフォルトとして記述し、reduce クエリに該当する場合に上書きします。グローバルな安全策と、対象を絞ったオーバーライドを組み合わせる方法がおすすめです。
/* Global safety net: neutralize runaway motion but DO NOT set 0s blindly,
which can break JS that waits for transitionend/animationend. Use 0.01ms. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important; /* stop infinite spins/marquees */
transition-duration: 0.01ms !important;
scroll-behavior: auto !important; /* kill smooth scroll */
}
}
グローバルな安全策は、あくまで最後の防護策です。段階3の動きを維持できるよう、その上に意図的に段階分けしたオーバーライドを重ねます。
.card {
transition: transform 400ms ease, opacity 400ms ease, background-color 200ms ease;
}
@media (prefers-reduced-motion: reduce) {
.card {
/* Tier 2: drop the transform (movement), keep opacity + color (Tier 3) */
transition: opacity 150ms ease, background-color 150ms ease;
}
.parallax-layer { transform: none !important; } /* Tier 1: remove */
.hero-zoom { animation: none !important; } /* Tier 1: remove */
}
凝ったエフェクトを適用する場合は、動きを有効にするために、肯定形のクエリ (prefers-reduced-motion: no-preference) を使用するのが最も安全なパターンです。
.hero { opacity: 1; } /* visible, static by default */
@media (prefers-reduced-motion: no-preference) {
.hero { animation: zoom-in 1.2s ease both; } /* only animate when allowed */
}
JS レイヤー:matchMedia による制御
CSS では、JavaScript によるアニメーション(GSAP のタイムライン、Framer の animate、canvas/WebGL、Lenis)を制御できません。同じ設定を matchMedia で読み取り、リアルタイムの変更にも対応してください(ユーザーがページを再読み込みせずに OS の設定を切り替える場合があります)。
const REDUCE_QUERY = '(prefers-reduced-motion: reduce)';
export function prefersReducedMotion() {
return typeof window !== 'undefined'
&& window.matchMedia
&& window.matchMedia(REDUCE_QUERY).matches;
}
// Live updates: listen so toggling the OS setting takes effect immediately.
export function onReducedMotionChange(cb) {
const mq = window.matchMedia(REDUCE_QUERY);
const handler = (e) => cb(e.matches);
mq.addEventListener('change', handler); // modern API
return () => mq.removeEventListener('change', handler);
}
ライブラリの制御を、再利用可能なレイヤーとして一括適用する
// GSAP — use matchMedia(): GSAP reverts conditional setups on change.
import gsap from 'gsap';
const mm = gsap.matchMedia();
mm.add({
motionOK: '(prefers-reduced-motion: no-preference)',
motionReduce: '(prefers-reduced-motion: reduce)',
}, (ctx) => {
const { motionOK } = ctx.conditions;
if (motionOK) {
gsap.from('.hero', { y: 80, opacity: 0, duration: 1 }); // Tier 1 move
} else {
gsap.from('.hero', { opacity: 0, duration: 0.15 }); // Tier 2 fade
}
});
// Lenis smooth scroll — do not instantiate at all when reduced.
import Lenis from 'lenis';
let lenis = null;
if (!prefersReducedMotion()) {
lenis = new Lenis();
const raf = (t) => { lenis.raf(t); requestAnimationFrame(raf); };
requestAnimationFrame(raf);
}
React:useReducedMotion フック
import { useState, useEffect } from 'react';
export function useReducedMotion() {
const query = '(prefers-reduced-motion: reduce)';
const get = () =>
typeof window !== 'undefined' && window.matchMedia
? window.matchMedia(query).matches
: false;
const [reduced, setReduced] = useState(get);
useEffect(() => {
const mq = window.matchMedia(query);
const onChange = () => setReduced(mq.matches);
onChange(); // sync after hydration
mq.addEventListener('change', onChange);
return () => mq.removeEventListener('change', onChange);
}, []);
return reduced;
}
// Framer Motion has its own useReducedMotion(); the hook above also works.
import { motion } from 'framer-motion';
function Card() {
const reduced = useReducedMotion();
return (
<motion.div
initial={reduced ? { opacity: 0 } : { opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: reduced ? 0.15 : 0.5 }}
/>
);
}
ハイドレーションの不一致を避けるため、SSR でレンダリングする状態は false(動きを有効にする)で初期化し、useEffect で再同期します。OS の設定値がわかるのはクライアント側のみです。
WCAG との対応
- 2.3.3 インタラクションによるアニメーション(AAA)。 インタラクション(スクロール、ホバー)によって発生する動きは、不可欠なものでない限り無効にできなければなりません。
prefers-reduced-motionを尊重するのが標準的な方法です。 - C39(CSS 技法)。
prefers-reduced-motionクエリを使用して動きを防止します。これは名称が定められた十分な技法です。 - 2.2.2 一時停止、停止、非表示(A)。 5秒を超えて続く自動再生の動きには、一時停止/停止の手段を用意する必要があります。自動カルーセルやマーキーは、メディアクエリの設定にかかわらずこの要件を満たさないため、操作コントロールも用意してください。
納品と検証(単体 HTML)
同梱ヘルパー(
scripts/):scripts/seek-shot.sh anim.html 0 1.5 3は?t=Nハーネスを使って各時点でアニメーションを停止し、スクリーンショットを撮影します。scripts/contact-sheet.sh sheet.png frame-*.pngは画像を並べて、ひと目で確認できる一覧を作成します。詳しくはscripts/README.mdを参照してください。
自己完結型のデモでは、納品物は ブラウザーで直接開ける 1 つの HTML ファイルです。マークアップ、CSS、必要な JS(CDN の GSAP/matchMedia)をインラインで記述し、ビルド手順は不要です。このスキルでは検証が重要です。通常の動作経路と視差効果を減らす動作経路の両方をスクリーンショットに撮り、段階的な処理が正しいことを確認してください。
出力要件:
- アニメーション全体と段階的な視差効果を減らすための上書き(CSS クエリと、JS 駆動のアニメーションには
matchMedia/gsap.matchMedia())を含む 1 つの.htmlファイル。 - フレームを停止するシーク用ハーネスと、スクリーンショット撮影時に視差効果を減らすコード経路を強制する方法。
シーク用ハーネス — フレームを停止し、設定を強制する。 ?t=N はタイムラインを停止します。?reduce=1 は OS 設定がオフでも、視差効果を減らすコード経路を強制します(CSS は JS から切り替えられないため、JS の実行条件に使うのと同じフラグを参照し、CSS レイヤーについては OS/ブラウザー設定をエミュレートしてスクリーンショットを撮影してください)。
<script>
const q = new URLSearchParams(location.search);
const reduce = q.get("reduce") === "1"
|| window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// build motion conditionally on `reduce` (Tier-1 removed, Tier-2 softened, Tier-3 kept)
const t = q.get("t");
if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); } // GSAP; CSS: el.style.animationDelay=(-t)+"s"; playState="paused"
window.__ready = true;
</script>
検証ループ — 両方の状態をスクリーンショットに撮る: 通常の動作経路と視差効果を減らす動作経路の両方で、アニメーションの途中を撮影し、Tier 1 の動きがなくなり、Tier 2 が短いフェードになり、Tier 3 が維持されていることを確認してください。CSS レイヤー用に設定をエミュレートしてください(Playwright の --reduced-motion=reduce を使用できます。エージェントのブラウザーツールでもエミュレートできます)。
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/demo.html?t=0.6" normal.png
npx playwright screenshot --reduced-motion=reduce --wait-for-timeout=500 "file://$PWD/demo.html?t=0.6&reduce=1" reduced.png
完了前に確認すること:
- 単体で開けること — コンソールエラーがなく、CDN(使用する場合)が読み込まれる。
- 停止処理と
?reduce=1(および--reduced-motion=reduce)で、両方のコード経路を確実に実行できる。 - アニメーションの途中を両方の状態で撮影していること — 通常時は意図した見た目で、視差効果を減らす場合は Tier 1 の動きがなくなり、Tier 2 が緩和されている。
prefers-reduced-motionはキルスイッチではなく、段階的な処理として適用すること — 瞬間移動させず、Tier 3 のフェード/フォーカスリング/スピナーは維持する。また、5 秒を超える自動アニメーションには一時停止コントロールを設けること(2.2.2)。- 両方の動作経路でイージングが意図どおりであること — 意図しない
linearを使わず、時間を適切に設定する(視差効果を減らす場合のフェードは ≤200ms、0.01msを0sにしない)。
クイックリファレンス
| 動きの種類 | Tier | 視差効果を減らす場合の動作 |
|---|---|---|
| パララックス/奥行き | 1 | 削除(transform: none) |
| 大きなスライド/移動 | 1 | 削除またはフェードに置き換え |
| 拡大縮小/ズーム(大きな動き) | 1 | 削除 |
| 回転/3D 回転 | 1 | 削除(animation: none) |
| 自動カルーセル/マーキー | 1 | 停止し、一時停止コントロールを用意(2.2.2) |
| スムーススクロール(Lenis) | 1 | 初期化しない |
| 小さなスライド/ポップイン | 2 | クロスフェード ≤200ms |
| 不透明度のフェード | 3 | 維持(長い場合は短縮) |
| 色/フォーカスリングのトランジション | 3 | 維持 |
| ローディングスピナー | 3 | 維持(不可欠なフィードバック) |
注意点
animation-duration: 0sは、animationend/transitionendを待つコードを壊すことがあります。イベントを発生させるために0.01msを使用してください。- グローバルな
*リセットだけでは Tier 3 の動きまで削除しすぎます。必ず意図した上書きを追加してください。 animation-iteration-count: 1を忘れると、無限回転がほぼゼロの時間で動き続けます(CPU 負荷が発生し、アニメーションも継続します)。- メディアクエリでは JS/canvas/WebGL を制御できません。
matchMediaで実行を制御しないと、ループが止まりません。 - OS の設定を切り替えてもページは再読み込みされません。
changeリスナーがないと、ページは古い状態のままです。 - SSR:レンダー中に
matchMediaを読み取るとエラーまたは不一致が発生します。デフォルトではアニメーションを有効にし、エフェクト内で同期してください。 - 自動再生するアニメーションには、視差効果を減らす設定がオフの場合でも、目に見える一時停止コントロールが必要です(2.2.2 はレベル A)。
参考ファイル
references/patterns.md— GSAP の完全なmatchMediaクリーンアップ、Framer Motion のMotionConfig、ライブ切り替え時の Lenis の開始/停止、再利用可能なanimateWithMotionPreferenceラッパー、一時停止コントロール付きマーキー、段階分類の監査チェックリスト。