スクロール連動の演出を盛り込むほど、「動きが苦手なユーザー」への配慮が必要になります。OS の「視差効果を減らす」設定(prefers-reduced-motion)を尊重することは、アクセシビリティの基本であり、実装も難しくありません。
まずフックで状態を取る
function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setReduced(mq.matches);
const handler = () => setReduced(mq.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return reduced;
}
「アニメしない」ではなく「即時に完成形を出す」
reduced-motion 時にアニメを止めるだけだと、opacity: 0 で始まる要素がずっと見えない事故が起きます。正しくは「アニメをスキップして最終状態を即時表示」です。
if (prefersReducedMotion) {
headingRef.current.style.clipPath = 'inset(0% 0 0 0)';
lines.forEach((el) => (el.style.opacity = '1'));
return; // GSAP も RAF も起動しない
}
これを怠ると、reduced-motion ユーザーにだけコンテンツが出ない、という最悪のリグレッションになります。
非同期 import との競合に注意
GSAP を動的 import している場合、解決を待つ間に effect が破棄されたり、reduced-motion が false → true に切り替わることがあります。解決後に active フラグを確認してから登録すると、リークや「止めたはずのアニメが動く」を防げます。
let active = true;
import('gsap').then(({ gsap }) => {
if (!active) return; // 破棄後/設定変更後は何もしない
// ...登録
});
return () => { active = false; };
重い描画はそもそも回さない
reduced-motion 時は、装飾的な canvas アニメーションや常時 RAF を起動しないのが一番です。可視判定(IntersectionObserver)と合わせ、isVisible && !prefersReducedMotion のときだけ描画するようにしました。
ホバー演出も「文字が読める」ことを最優先に
カードのホバーで背景を暗くして文字色を変える、といった演出は、状態の組み合わせ次第で文字が読めなくなることがあります。GSAP が transform を握っている要素では、ホバーを transform で動かすと競合するので、box-shadow / border / 背景色など競合しないプロパティで表現し、かつコントラストを保つよう設計しました。
まとめ
- reduced-motion は「止める」ではなく「即時に完成形を出す」
- 非同期 import 時は
activeフラグで競合を防ぐ - 重い描画は起動しない・画面外で止める
- ホバーはコントラストと競合に注意
演出とアクセシビリティは両立できます。むしろ両立させる前提で設計すると、コードの後始末も綺麗になります。
ご相談はこちらから。