概要
アニメーション終了を検知する際、animationend
イベントを使用することがある。しかし、アクセシビリティの問題が潜んでいる。
アニメーション終了を検知する際、animationend
イベントを使用することがある。しかし、アクセシビリティの問題が潜んでいる。
アニメーションを伴うモーダルやダイアログを閉じる際、以下のような実装をよく見かける。
modal.classList.add('fade-out');
modal.addEventListener('animationend', () => {
modal.remove();
setState('closed');
});
ユーザーがシステムで「視覚効果を減らす」設定を有効にしている場合、以下のような CSS を適用する。このような設定は、モダンなリセットCSSにも含まれていることがある。
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
animationend
イベントとprefers-reduced-motion
との組み合わせにより次の問題が発生する。
animationend
イベントが発火しない同様の問題は他の状況でも発生する可能性がある。
animationend
イベントには、prefers-reduced-motion
以外にも以下の問題がある。
animation-iteration-count: infinite
ではanimationend
が発火しないこれらの問題からも、animationend
への依存は避けるべきである。
スクリーンリーダーを使用するユーザーにとって、視覚的なアニメーションは意味を持たない。しかし、アニメーション完了に依存した処理は、これらのユーザーにも影響を与えてしまう。
一部の実装では、アニメーションを無効化せず、極めて短い時間(0.01ms等)に設定する方法もある。
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}
この方法ならanimationend
イベントは発火するが、以下の問題がある。
そのため、本記事ではsetTimeout
を推奨する。
setTimeout
を使用する。
animationend
を使用している既存のコードは、以下の手順で安全に移行できる。
// Step 1: 既存の実装
modal.addEventListener('animationend', handleEnd);
// Step 2: setTimeout併用版
let done = false;
modal.addEventListener('animationend', () => {
if (!done) {
done = true;
handleEnd();
}
});
setTimeout(() => {
if (!done) {
done = true;
handleEnd();
}
}, 300);
// Step 3: 最終版
setTimeout(handleEnd, 300);
animationend
の代わりに setTimeout
を使用することで、確実に処理を実行できる。
// ✅ 改善された実装
modal.classList.add('fade-out');
setTimeout(() => {
modal.remove();
setState('closed');
}, 300); // アニメーション時間と同じ値
この方法なら基本的な問題は解決するが、アニメーション時間とタイマーの同期が課題となる。
さらにアクセシビリティを考慮した完全な実装は以下のようになる。
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const animationDuration = prefersReducedMotion ? 0 : 300;
modal.classList.add('fade-out');
setTimeout(() => {
modal.remove();
setState('closed');
}, animationDuration);
再利用可能なカスタムフックとして実装する。
import { useState, useEffect, useCallback } from 'react';
const useAccessibleAnimation = (duration = 300) => {
const [animating, setAnimating] = useState(false);
const [reduced, setReduced] = useState(false);
useEffect(() => {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
setReduced(mq.matches);
const onChange = (e) => setReduced(e.matches);
mq.addEventListener('change', onChange);
return () => mq.removeEventListener('change', onChange);
}, []);
const delay = reduced ? 0 : duration;
const start = useCallback(
(callback) => {
setAnimating(true);
setTimeout(() => {
setAnimating(false);
callback?.();
}, delay);
},
[delay],
);
return { animating, start, delay };
};
カスタムフックを使用したモーダルの実装例。
const Modal = ({ isOpen, onClose, children }) => {
const [closing, setClosing] = useState(false);
const { start, delay } = useAccessibleAnimation(300);
const close = () => {
setClosing(true);
start(() => {
setClosing(false);
onClose();
});
};
if (!isOpen && !closing) return null;
return (
<div className={`modal ${closing ? 'closing' : ''}`}>
{children}
<button onClick={close}>Close</button>
</div>
);
};
CSS 側でもアクセシビリティを考慮する。
.modal {
/* 基本スタイル */
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.modal--closing {
animation: fadeOut var(--animation-duration, 300ms) ease-out;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
/* アニメーション無効時は即座に非表示 */
@media (prefers-reduced-motion: reduce) {
.modal--closing {
animation: none;
opacity: 0;
}
}
システム設定を知らないユーザーや、借りたデバイスを使用している場合を考慮し、サイト内でアニメーションを制御できる機能を提供することも重要である。
const useAnimationPreference = () => {
const [reduced, setReduced] = useState(() => {
const saved = localStorage.getItem('reduceMotion');
return saved === 'true' || window.matchMedia('(prefers-reduced-motion: reduce)').matches;
});
const toggle = () => {
const value = !reduced;
setReduced(value);
localStorage.setItem('reduceMotion', String(value));
};
return { reduced, toggle };
};
これにより、ユーザーは自分の好みに応じてアニメーションを制御でき、よりアクセシブルな体験を提供できる。
animationend
イベントは便利だが、アクセシビリティの観点から注意が必要である。setTimeout
を使用し、prefers-reduced-motion
を考慮することで、すべてのユーザーにとって確実に動作するインターフェースを実装できる。