Safariでdata属性変更時にCSS transitionが2回目以降動作しない問題と解決策
Safari(macOS、iOS、iPadOS)でdata-*属性の変更をトリガーにしたCSS transitionが、2回目以降動作しないという問題に遭遇しました。この記事では問題の詳細と解決策について解説します。
問題の詳細
発生状況
モーダルの開閉時にhtml[data-open-modal="true"]でアニメーションをトリガーするような実装をしていた際、以下の現象が発生しました。
- 1回目は正常にtransitionが動作する
- 2回目以降、transitionが発火しない
- Firefox/Chromeでは問題なし、Safariのみで発生
問題の再現コード
.character { opacity: 0; translate: 0 30px;
html[data-open-modal='true'] & { opacity: 1; translate: 0; transition: opacity 0.3s, translate 0.3s; }}
// モーダルを開くpublic open(): void { this.modalElement.showModal() document.documentElement.dataset.openModal = 'true' // ← 2回目以降transitionが発火しない}
data属性を変更してスタイルを切り替える実装自体は一般的ですが、Safariではスタイルのキャッシュが原因なのか、2回目以降のdata属性変更時にtransitionが正しくトリガーされないことがあります。
解決策
2つの解決策を紹介します。
方法1: 強制的なreflow(簡単)
offsetHeightを読み取ることでreflowを強制し、transitionを確実にトリガーする方法です。
public open(): void { this.modalElement.showModal()
// 強制的なreflowでtransitionを確実にトリガー(Safari対策) void this.modalElement.offsetHeight
document.documentElement.dataset.openModal = 'true'}
ポイント:
offsetHeightを読み取ると、ブラウザは最新のレイアウト情報を計算するためにreflowを実行する- これにより要素のスタイル状態がリセットされ、次のスタイル変更時にtransitionが確実にトリガーされる
voidは戻り値を無視することを明示(ESLintの未使用変数警告を回避)
この方法は1行追加するだけで済むため、既存のコードへの影響が最小限で済みます。
方法2: CSS animation(@keyframes)を使用
transitionではなくanimationを使う方法です。CSS animationはセレクタにマッチするたびに再生されるため、2回目以降も確実に動作します。
@keyframes fade-in { from { opacity: 0; translate: 0 30px; } to { opacity: 1; translate: 0; }}
.character { opacity: 0; translate: 0 30px;
html[data-open-modal='true'] & { animation: fade-in 0.3s ease forwards; }}
ポイント:
animationは条件にマッチするたびに最初から再生されるforwardsを指定することで、アニメーション終了後の状態を維持できる- CSSのみで完結するため、JavaScript側の修正が不要
なぜreflowで解決するのか
ブラウザはパフォーマンス最適化のため、連続するスタイル変更をバッチ処理しようとします。Safariではこの最適化が強く働くためか、data属性の変更とそれに伴うスタイル変更が適切にtransitionをトリガーしないことがあります。
offsetHeightなどのレイアウト情報を読み取ると、ブラウザは保留中のスタイル変更をすべて適用し、レイアウトを再計算(reflow)する必要があります。これにより:
- 現在のスタイル状態が確定する
- 次のスタイル変更が「新しい変更」として認識される
- transitionが正しくトリガーされる
という流れで問題が解決します。
まとめ
Safariでdata属性変更時にCSS transitionが2回目以降動作しない問題には、以下の解決策があります。
| 方法 | メリット | デメリット |
|---|---|---|
| reflow強制 | 1行追加で済む、既存CSSを変更不要 | パフォーマンスへの影響(軽微) |
| CSS animation | CSSのみで完結、JS修正不要 | 既存のtransitionをanimationに書き換え必要 |
個人的には方法1のreflow強制が最も手軽でおすすめです。1行追加するだけで済み、既存のCSSを変更する必要もありません。
Safari固有の挙動に悩まされた際は、ぜひ試してみてください。
参考リンク
MDN Web Docs
- HTMLElement: offsetHeight property
- Using CSS transitions
- CSS Animations
- animation-fill-mode
- void operator