mcpField
A
accessible-animation

accessible-animation

github.com/iart-aiデザイン・UI632.7k 回インストール18 週間前に更新
リポジトリ

accessible-animation について

動きを減らす設定に対応したウェブアニメーションの作成・監査向けスキルです。動きを削除・緩和・維持の3段階に分け、CSSやJavaScriptの実装例とスクリーンショットによる検証方法を示します。

主な機能

  1. 01動きを減らす3段階の指針
  2. 02CSSメディアクエリのパターン
  3. 03JavaScript matchMedia制御
  4. 04Reactの動きを減らすフック
  5. 05単体デモの検証

活用シーン

  1. 01不快感を招くアニメーションの監査
  2. 02GSAPやFramer Motionの効果の調整
  3. 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

完了前に確認すること:

  1. 単体で開けること — コンソールエラーがなく、CDN(使用する場合)が読み込まれる。
  2. 停止処理と ?reduce=1(および --reduced-motion=reduce)で、両方のコード経路を確実に実行できる。
  3. アニメーションの途中を両方の状態で撮影していること — 通常時は意図した見た目で、視差効果を減らす場合は Tier 1 の動きがなくなり、Tier 2 が緩和されている。
  4. prefers-reduced-motion はキルスイッチではなく、段階的な処理として適用すること — 瞬間移動させず、Tier 3 のフェード/フォーカスリング/スピナーは維持する。また、5 秒を超える自動アニメーションには一時停止コントロールを設けること(2.2.2)。
  5. 両方の動作経路でイージングが意図どおりであること — 意図しない 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 ラッパー、一時停止コントロール付きマーキー、段階分類の監査チェックリスト。

コメント