prefers-reduced-motion(モーション軽減設定)
問題
アニメーションやトランジションは、前庭障害、モーション感度、特定の認知障害を持つユーザーに不快感、めまい、吐き気を引き起こす可能性があります。prefers-reduced-motion メディアクエリは、ユーザーがオペレーティングシステムの設定を通じてモーションの好みを伝えることを可能にします。AIエージェントは生成するコードにモーション設定の処理をほとんど含めず、含める場合でもすべてのモーションを完全に削除する傾向があります。しかし、これは有用な状態変化インジケーターまで削除してしまうため、逆にユーザビリティを損なう可能性があります。
解決方法
prefers-reduced-motion: reduce の設定を尊重し、モーションを削除するのではなく軽減しましょう。大きく速いアニメーションやパララックススタイルのアニメーションを、微妙なフェードや即時の状態変化に置き換えます。フォーカスリングやローディング状態のような機能的なインジケーターはそのまま維持しましょう。
2つのアプローチ
- モーション削除アプローチ:通常通りアニメーションを書き、
prefers-reduced-motion: reduceブロック内で無効化します。 - ノーモーションファーストアプローチ:デフォルトで静的なスタイルを書き、
prefers-reduced-motion: no-preferenceブロック内でアニメーションを追加します。この方が安全です。設定を指定していないユーザーでもモーションが軽減されるためです。
prefers-reduced-motion: フルモーション vs 軽減モーション
コード例
グローバルな軽減モーションリセット
モーション軽減を希望するユーザー向けに、すべてのアニメーションを軽減する防御的リセットです:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
これは大まかなツールです。ベースラインとして使用し、必要に応じて個々のコンポーネントを調整しましょう。
モーションをフェードに置き換える(より良いアプローチ)
すべてのアニメーションを削除する代わりに、大きなモーションを微妙な透明度変化に置き換えます:
/* Default: slide-in animation */
.modal {
animation: modal-enter 0.3s ease-out;
}
@keyframes modal-enter {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Reduced motion: fade only, no spatial movement */
@media (prefers-reduced-motion: reduce) {
.modal {
animation: modal-fade-in 0.2s ease-out;
}
@keyframes modal-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
ノーモーションファーストアプローチ
アニメーションなしの状態から始め、モーション設定がないユーザーにのみアニメーションを追加します:
/* Base: static, no animation */
.card {
opacity: 1;
transform: none;
}
/* Only animate for users without motion preference */
@media (prefers-reduced-motion: no-preference) {
.card {
animation: card-reveal 0.4s ease-out both;
}
@keyframes card-reveal {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
安全なトランジション
.button {
background-color: var(--color-primary);
}
/* Hover transition: only for no-preference users */
@media (prefers-reduced-motion: no-preference) {
.button {
transition: background-color 0.15s ease, transform 0.15s ease;
}
}
@media (hover: hover) {
.button:hover {
background-color: var(--color-primary-dark);
}
}
/* Reduced motion users still see the color change, just instantly */
ローディングスピナーの代替
.spinner {
width: 2rem;
height: 2rem;
border: 3px solid var(--color-border);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Reduced motion: pulsing opacity instead of spinning */
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
}
スクロール動作
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
パララックスとスクロール駆動アニメーション
.hero__background {
animation: parallax linear;
animation-timeline: scroll();
}
@keyframes parallax {
from {
transform: translateY(-15%);
}
to {
transform: translateY(15%);
}
}
/* Disable parallax entirely for reduced motion */
@media (prefers-reduced-motion: reduce) {
.hero__background {
animation: none;
transform: none;
}
}
JavaScriptでの検出
JavaScriptで制御されるアニメーションの場合:
<script>
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
);
function handleMotionPreference() {
if (prefersReducedMotion.matches) {
// Disable JS-driven animations
document.documentElement.dataset.reducedMotion = "true";
} else {
delete document.documentElement.dataset.reducedMotion;
}
}
prefersReducedMotion.addEventListener("change", handleMotionPreference);
handleMotionPreference();
</script>
/* Use the data attribute for JS-controlled animations */
[data-reduced-motion="true"] .js-animated {
animation: none !important;
transition: none !important;
}
維持すべきもの vs 軽減すべきもの
/* KEEP: Focus indicators (functional, not decorative) */
.button:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
/* No transition needed — instant is fine */
}
/* KEEP: Color changes (not spatial motion) */
@media (prefers-reduced-motion: reduce) {
.button:hover {
/* Color change is fine, remove transform */
background-color: var(--color-primary-dark);
transform: none;
}
}
/* REDUCE: Large spatial movement */
@media (prefers-reduced-motion: reduce) {
.slide-in-panel {
/* Replace slide with fade */
animation: fade-in 0.15s ease;
}
}
/* REMOVE: Parallax, background movement, continuous animations */
@media (prefers-reduced-motion: reduce) {
.background-animation,
.parallax-layer,
.floating-element {
animation: none;
}
}
AIがよくやるミス
prefers-reduced-motionを一切含めない:最もよくあるミスです。AIはモーション設定の処理なしでアニメーションを生成します。- 一括ルールですべてのアニメーションを削除する:すべてのアニメーションとトランジションを無効にすると、有用な状態インジケーターまで削除されます。モーションは削除ではなく軽減しましょう。
scroll-behavior: autoを忘れる:軽減モーションユーザー向けのオプトアウトなしでscroll-behavior: smoothを設定してしまいます。- 削除したアニメーションの代替を提供しない:スライドインアニメーションを削除しながら、フェードの代替を提供せず、ユーザーに状態変化のインジケーターがなくなります。
- CSSアニメーションのみ対応する:JavaScriptで制御されるアニメーション(GSAP、Framer Motionなど)もこの設定を尊重する必要があることを忘れてしまいます。
- デフォルト状態のみテストする:軽減モーションを有効にした場合の体験を検証しません。Chrome DevToolsでエミュレートできます:Rendering パネル > Emulate CSS media feature > prefers-reduced-motion: reduce。
使い分け
- アニメーションのあるすべてのプロジェクト:アニメーションやトランジションを追加する場合は、必ず
prefers-reduced-motionの処理を追加しましょう。 - パララックスとスクロールエフェクト:軽減モーションユーザーに対しては常に無効にすべきです。
- 自動再生アニメーション:フローティング要素や背景エフェクトなどの継続的な装飾アニメーションは停止すべきです。
- ページトランジション:フルページのルートトランジションはシンプルなフェードに軽減するか、削除すべきです。
- 機能的なモーションは維持する:ローディングインジケーター、フォーカスリング、状態変化インジケーターは保持しましょう(簡略化は可能ですが、削除は避けましょう)。