現代のウェブサイトは、かつてないほど壮大なビジュアルで注目を集めようと競い合っています。パララックス(視差効果)を用いたヒーローセクションはビューポートの奥深くまで広がり、カルーセルは際限なくスクロールし、サインアップフォームの背後では背景動画が自動再生されます。デザインポートフォリオで見れば、これらのパターンは洗練されていて魅力的に映ります。しかし、一部のユーザーにとっては、これらは身体的なトリガーとなります。

前庭疾患(ぜんていしっかん)を抱える人々は、広範囲にわたる連続的な動きに直面すると、吐き気、めまい、または偏頭痛を感じることがあります。彼らを保護するために、現在すべての主要なオペレーティングシステムには「視覚効果を減らす(Reduce Motion)」というアクセシビリティ設定が含まれています。ウェブブラウザは、この設定を prefers-reduced-motion メディアクエリを通じて公開しています。これをクリーンに尊重したいReact開発者は、@reactuses/coreuseReducedMotion フックを利用できます。このフックはOSの設定をブール値として読み取り、ユーザーが設定を変更した場合にはリアルタイムで更新し、サーバーサイドレンダリング(SSR)時でもクラッシュすることなく処理を行います。

自作のソリューションが失敗する理由

window.matchMedia('(prefers-reduced-motion: reduce)') を使って、自分で設定をクエリすることも可能です。多くの開発者がそうしていますが、その多くが微妙なバグを抱えたままリリースしてしまいます。

第一に、変更リスナー(change listener)の登録を忘れがちであることです。初期クエリはコンポーネントのマウント時に一度だけ実行されます。もしユーザーがアプリを開いた後、カルーセルの動きで気分が悪くなったためにシステム設定で「視覚効果を減らす」を切り替えたとしても、コンポーネントはその更新を検知できません。メディアクエリオブジェクトは addEventListener をサポートしていますが、正しく接続し、アンマウント時に削除し、さらにレガシーな addListener 構文のポリフィルを用意するといった作業は、コピー&ペーストによるミスを誘発しやすいボイラープレート(定型コード)となります。

第二に、SSR中には window が存在しないことです。アプリがサーバー上でレンダリングされる場合、単純な matchMedia の呼び出しは参照エラー(reference error)を発生させ、レンダリングを停止させてしまいます。その結果、チェックを typeof window !== 'undefined' で囲んでガードし、デフォルト値を推測し、クライアント側でのハイドレーションが一致することを祈るような実装になってしまいます。

第三に、アニメーションを行うすべてのコンポーネントでそのロジックを繰り返すと、メンテナンス上の負債となります。あるチームメンバーはリスナーを特定の方法で書き、別のメンバーは完全に省略し、また別のメンバーは動きを優先するデフォルト値をハードコードするといったことが起こり得ます。フックを使うことで、この混乱を中央集約化できます。

useReducedMotion はこれら3つの問題をすべて解決します。単純なブール値を返し、サーバー上では安全に何もしません(no-op)。そして、リスナーの登録とクリーンアップを自動的に行います。

実践するための3つの方法

ブール値を取得したら、それに基づいてどのように動作させるかという戦略が必要です。単一のコンポーネントからアプリケーション全体まで拡張可能な、3つのパターンを紹介します。

1. CSSクラスを制御して無駄な処理を避ける

最も直接的なアプローチは、重いJavaScriptのアニメーションループ自体を開始させないことです。例えば、通常はスクロールイベントを監視し、requestAnimationFrame ループ内でレイヤーを移動させるパララックス画像コンポーネントがあるとします。そのロジックを実行してから視覚的な結果を抑制するのではなく、まず useReducedMotion をチェックします。

フックが true を返す場合は、静的なバリアントクラスを使用してコンポーネントをレンダリングし、スクロールリスナーをバインドする useEffect をスキップします。これにより、ブラウザの負荷が軽減されます。動きに敏感なユーザーには固定された画像が表示され、それ以外のユーザーには動くレイヤーが表示されます。アニメーションコード自体が初期化されないため、CPUやバッテリーの節約にもつながります。

2. アニメーションライブラリに直接渡す

Framer Motion のようなライブラリを使用している場合、このフックはプロップ(prop)の定義にそのまま組み込めます。Motionコンポーネントは、バリアント、トランジション、ジェスチャーの設定オブジェクトを受け入れます。useReducedMotion から得られるブール値を使用して、トランジションの duration: 0 を条件付きで設定したり、あるいは