タグアーカイブ アニメーション

CSS translateY()の基本と使い方、垂直移動をマスター

CSS translateY()の基本と使い方、垂直移動をマスター

translateY() の基本構文と引数

translateY() の基本構文と引数

translateY() は CSS の transform プロパティで使用する関数の一つだ。指定した値の分だけ要素を垂直方向に移動させる。正の値なら下へ、負の値なら上へ動く。

構文の基本

構文は極めてシンプルだ。

transform: translateY(値);

値には長さ(px, em, rem など)またはパーセンテージを指定できる。パーセンテージは要素自身の高さを基準とする。例えば高さ 100px の要素に translateY(50%) を指定すると、50px 下に移動する。

正の値で下へ移動
元の位置
30px 下
※ 青い要素が translateY(30px) で下にずれている
負の値で上へ移動
元の位置
20px 上
※ 赤い要素が translateY(-20px) で上にずれている

なお、親要素の高さや余白には一切影響しない。あくまで視覚的な位置だけが変わる点が重要な特徴だ。

length と percentage の使い分け

length(px, rem など)は固定量の移動が必要な場合に使う。一方、percentage は要素のサイズに応じて相対的に位置を変えたいときに便利だ。たとえば、高さが可変するコンテナ内で要素を半分だけ上にずらすには translateY(-50%) と書く。この手法はモーダルやツールチップの中央揃えでもよく使われる。

percentage の例:要素の高さの50%分だけ上へ
要素の中央に配置(translateY(-50%))
紫色のボックスがコンテナの中央にピッタリ配置されている。

アニメーションへの応用、カードのスライドイン

アニメーションへの応用、カードのスライドイン

translateY() は CSS アニメーションやトランジションと組み合わせることで、自然な動きを生み出せる。特に「下からスライドイン」「フェードインしながら上昇」といった演出に適している。

カードの表示アニメーション

たとえば、ダッシュボードに統計カードを並べる場合を考えてみよう。初期状態では各カードを少し下にずらし、透明度を 0 にしておく。ページが読み込まれたタイミングやスクロールに合わせて translateY(0) かつ opacity: 1 へトランジションさせる。

.stat-card {
  opacity: 0;
  transform: translateY(50px);
  transition: opacity 0.8s ease-in, transform 0.8s ease-in;
}

.dashboard.active .stat-card {
  opacity: 1;
  transform: translateY(0);
}
アニメーション前(Before)
カード(非表示・ずれ)
※ opacity:0.3、translateY(20px) の状態
アニメーション後(After)
カード(表示・定位置)
※ opacity:1、translateY(0) で完全表示

この変化が実際は 0.8 秒かけて連続的に行われる。ユーザーは要素が「下から浮き上がってくる」ように感じる。

ホバー時のマイクロインタラクション

表示されたカードにマウスを重ねたとき、ほんの少し上へ移動させる演出もよく使われる。translateY() に負の値を指定すれば良い。

.dashboard.active .stat-card:hover {
  transform: translateY(-8px);
}
ホバー前(Before)
カード(通常位置)
ホバー後(After)
カード(8px 上昇)
※ 要素が浮き上がり、影も付くことで立体感が増す

このわずかな動きが、クリック可能な要素であることを直感的に伝える。影(box-shadow)の変化と組み合わせると、よりリッチな表現になる。

フォームラベルの移動アニメーション

フォームラベルの移動アニメーション

translateY() は UI コンポーネントの動きを設計する上でも重宝する。代表例が、入力フィールドのラベルがフォーカス時に上へ移動する「フローティングラベル」パターンだ。

ラベルの初期配置

まず、label 要素を input フィールドの内側に絶対配置で重ねる。ポインターイベントを無効化しておけば、ラベルをクリックしても input にフォーカスが当たる。

label {
  position: absolute;
  left: 15px;
  top: 15px;
  pointer-events: none;
  transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}

フォーカス時の移動

input にフォーカスが当たったとき、もしくはプレースホルダーが表示されていない(入力済み)ときに、ラベルを translateY(-32px) で上へ移動させる。同時に少し縮小すると、より洗練された動きになる。

input:focus ~ label,
input:not(:placeholder-shown) ~ label {
  transform: translateY(-32px) scale(0.8);
  color: #6200ee;
  font-weight: bold;
}
フォーカス前(Before)
お名前
 
※ ラベルがプレースホルダーのように表示されている
フォーカス後(After)
お名前
山田太郎
※ ラベルが上に移動し、小さくなっている

このアニメーションは実際には約 0.25 秒でスムーズに行われる。ラベルが単に上に動くだけでなく、縮小と色の変化が加わることで、UI に統一感が生まれる。

translateY() の特性、他の要素に影響しない

translateY() の特性、他の要素に影響しない

transform 系の関数全般に言えることだが、translateY() はドキュメントフローを一切変更しない。つまり、要素を移動させても周囲の要素は元の位置を保ったままになる。

ドキュメントフローへの影響

例えば、3つのブロックが縦に並んでいるとする。中央のブロックに translateY(40px) を適用しても、上下のブロックはピクリとも動かない。あくまで中央のブロックの「描画位置」だけが変わる。

上のブロック
移動したブロック(translateY(40px))
下のブロック
※ 真ん中のブロックが下にずれているが、上下のブロックは元の位置のまま。

margin との違い

似たような位置調整として margin-top がある。しかし margin は要素自体の「占有領域」を変化させるため、後続の要素が押し出される。レイアウト全体に影響を与えたくない場合、translateY() のほうが安全だ。

/* 非推奨: レイアウトシフトを起こす */
.element {
  margin-top: 30px;
}

/* 推奨: 描画位置だけを変える */
.element {
  transform: translateY(30px);
}

また、translateY() は GPU による合成処理が行われるため、アニメーションのパフォーマンス面でも優れる。リフローやリペイントが発生しないからだ。頻繁に動かす要素には積極的に使いたい。

ポインター擬似クラスでの問題と解決策

ポインター擬似クラスでの問題と解決策

translateY() を :hover に直接指定すると、意図しないちらつきを起こすことがある。CSS-Tricks の記事でも指摘されている点だ。

ちらつきが起きる仕組み

ホバー時に要素を大きく移動させると、マウスカーソルが要素から外れてしまう。すると :hover 状態が解除されて要素が元の位置に戻り、再びカーソルが要素に重なってまた移動する……という無限ループが発生する。

問題のあるコード(hover が要素についている)
.bad:hover { transform: translateY(160px); }
※ 要素が大きく動き、カーソルが外れて戻るを繰り返す
解決策(親要素に hover を付ける)
.parent:hover .good { transform: translateY(160px); }
※ 親要素がホバー領域を担保するので安定する

親要素で包む解決策

最も簡単な対策は、移動させたい要素を親コンテナでラップし、:hover 擬似クラスを親に適用することだ。これでホバー領域が親のサイズで固定され、子要素がどこに移動してもカーソルが外れにくくなる。

.card-container:hover .card {
  transform: translateY(-12px);
}

この書き方は、ホバーでカードが浮き上がる演出など、あらゆる translateY() アニメーションに応用できる。親要素のサイズに余裕を持たせておけば、より大きな変位にも対応可能だ。

この記事のポイント

  • translateY() は要素を垂直方向に移動させる。正の値で下、負の値で上。
  • パーセンテージ指定は要素自身の高さが基準。中央配置などに便利。
  • アニメーションでは opacity と組み合わせ、スライドインに使える。
  • フォームラベルの浮上アニメーションは translateY(-32px) と scale(0.8) で実装。
  • ドキュメントフローを壊さないため、margin よりパフォーマンスが良い。
  • :hover のちらつきは親要素に擬似クラスを付ければ解消する。
CSS translateZ()の基本と実践、perspectiveで3D表現をマスター

CSS translateZ()の基本と実践、perspectiveで3D表現をマスター

CSSのtransformプロパティに指定できるtranslateZ()関数は、要素をZ軸方向へ移動させるものである。従来のブラウザ表示は縦と横の2次元だが、translateZ()perspective(遠近感)を組み合わせることで、奥行きのある3次元的な表現が可能になる。ただし、perspectiveが設定されていないとtranslateZ()は何の変化も起こさない点が、多くの開発者がつまずく最初のポイントだ。

translateZ()を正確に使いこなせば、カードの裏表切り替えや立体カルーセルといったUIの表現力が高まる。さらに、translateZ(0)という小さな指定が、アニメーションのちらつきを抑え、GPUによる高速レンダリングを引き出すパフォーマンスハックとして知られている。本記事では、この関数の基本動作から実践的な活用テクニックまでを図解とともに解説する。

translateZ() の基本動作と perspective の必須条件

translateZ() の基本動作と perspective の必須条件

translateZ()は、要素をZ軸(画面の手前または奥)方向に指定した距離だけ移動させる。値が正であれば手前に近づき、負であれば奥に遠ざかる。しかし、単独でtransform: translateZ(100px);と書いても、見た目は何も変わらない。これは、ブラウザがデフォルトで3次元空間の遠近感を持たず、あらゆる要素を画面上に平坦に描画しているためである。

perspective がなければ効果はゼロ

translateZ()の効果を発揮させるには、対象の要素に対して「どれだけ離れて見ているか」を決めるperspective(遠近法の視点距離)の設定が必須になる。perspectiveは、要素の親にプロパティとして指定する方法と、transformの関数perspective()として一時的に指定する方法の2通りがある。まずは、親要素にperspectiveプロパティを設定した場合のコードを見てみよう。

.scene {
  perspective: 800px;
}

.box {
  transform: translateZ(100px);
}

このようにすると、.scene内の全子要素に800pxの視点距離が適用され、その中で.boxが100px手前に移動する。つまり.boxは画面上で拡大されたかのように表示される。しかし、実際の大きさが変わるわけではなく、視点からの距離が短縮された結果、見かけ上のスケールが変わるという理屈である。

perspective なし(Before)
Box
perspective あり(After)
Box

上のデモが示す通り、perspectiveを設定していない環境ではtranslateZ(100px)は全く反映されず、元のサイズのままである。一方、perspective:500pxの親で囲むと、ボックスが手前に飛び出して大きくなったように見える。これがZ軸移動の基本的な仕組みである。

perspective プロパティと perspective() 関数の使い分け

perspectiveはプロパティとして親に指定することで、その配下にある複数の3D変形要素に共通の視点距離を与える。一方、perspective()関数は特定のtransform指定の中でのみ有効で、ほかの要素には影響しない。また、関数を使う際は記述順序に注意が必要である。必ずperspective()translateZ()より先に書かなければ、視点距離が適用されない。

/* NG: 後方にあると無効 */
transform: translateZ(100px) perspective(800px);

/* OK: 前方に記述 */
transform: perspective(800px) translateZ(100px);

全体の3Dシーンを統一した視点で扱いたい場合はperspectiveプロパティ、特定の要素だけに局所的な遠近感を与えたい場合はperspective()関数を用いると覚えておくとよい。

translateZ() の実践的な使い方

translateZ() の実践的な使い方

ここからは、実際の開発で役立つtranslateZ()の応用を見ていく。まず、3D空間での奥行きをより直感的に理解できるように、親要素ごと回転させたデモを用意した。

奥行きを可視化する3Dデモ

親コンテナにrotateY()で角度を付け、さらにtransform-style: preserve-3dを指定することで、子要素が3D空間内でどの位置にあるかが一目でわかる。次のデモでは、translateZ(0)(奥行きなし)とtranslateZ(100px)(手前への移動)を比較している。

translateZ(0)(Before)
Box
translateZ(100px)(After)
Box

回転された親の内部で、translateZ(100px)のボックスは手前に飛び出していることが確認できる。要素の横幅や高さのピクセル値そのものは変わっていない。視点距離とZ軸方向の移動量によって、画面上への投影サイズが変化するというのが、translateZ()の本質である。

パフォーマンスハックとしての translateZ(0)

translateZ(0)はZ軸方向へ0ピクセル移動する指定であり、本来は何も変わらない。ところが、この小さな宣言がブラウザのレンダリングエンジンに「3D変形が使われている」と認識させ、対象要素の描画をCPUからGPUへ切り替えさせるトリガーとなる。GPUはグラフィック処理に特化したハードウェアであり、アニメーションやトランジションの再描画を高速に行える。

.animated-box {
  transform: translateZ(0);
  /* これでレンダリングがGPUに委譲され、ちらつきが抑制される */
}

特に、CSSアニメーションで要素がガタつく(ジャンク)現象が発生している場合、translateZ(0)を追加するだけで症状が改善することが多い。ただし、GPUへの過度な依存はメモリ消費を増やすため、必要な要素に絞って適用するのが望ましい。

translateZ() と scale() の混同に注意

translateZ() と scale() の混同に注意

translateZ()を適用すると要素が拡大したように見えるため、「scale()と同じではないか」という誤解を招きがちだ。しかし、両者は根本的に異なる概念であるscale()は要素そのものの寸法を乗算で拡大・縮小するのに対し、translateZ()は遠近法による投影上の見かけの大きさを変えるだけだ。

scale(1.2) の場合(実寸が変わる)
Scale
translateZ(100px) の場合(見かけのみ)
TranslateZ

この違いは、レイアウト計算や重なり順の制御において重要だ。scale()による拡大は要素のボックスサイズを変化させ、周囲の要素を押しのける可能性があるが、translateZ()による見かけの拡大はレイアウトフローに影響を与えない。3D変形を使う以上、どの操作が実際の寸法に関わるかを理解しておく必要がある。

ブラウザサポートと仕様

ブラウザサポートと仕様

translateZ()関数はCSS Transforms Module Level 2で定義されており、主要なモダンブラウザ(Chrome、Firefox、Safari、Edge)すべてでサポートされている。Internet Explorer 11は3D変形に対応していないが、現在のユーザーシェアから考えると実務上の大きな制約にはならない。また、perspectiveプロパティやtransform-style: preserve-3dとの併用も各ブラウザで一貫した動作を示す。

ただし、古いモバイルOSのブラウザや非常に限定的な環境では、GPUアクセラレーション周りの挙動に差異が生じる場合がある。実装時には実機テストを行い、パフォーマンス面のキャッチアップを心がけたい。

この記事のポイント

  • translateZ()はZ軸方向への移動を行い、perspectiveがなければ効果は現れない
  • 遠近感の設定はperspectiveプロパティ(親要素)とperspective()関数(要素自身)の2方式がある
  • 要素の実寸を変えるscale()とは異なり、投影上の大きさを変化させる
  • translateZ(0)はGPUレンダリングを誘発し、アニメーションの性能改善に役立つ
CSSスクロールトリガーアニメーション入門、Chrome 146で実装

CSSスクロールトリガーアニメーション入門、Chrome 146で実装

Chrome 146で、CSSによるスクロールトリガーアニメーションが実装された。これは要素がビューポート内に入ったことをきっかけに、指定した時間だけアニメーションを再生する仕組みだ。従来のスクロール駆動アニメーションとは動作原理が異なる。

スクロール駆動アニメーションが「スクロール位置に合わせてアニメーションの進行度が変化する」のに対し、スクロールトリガーアニメーションは「特定のスクロール地点でアニメーションを発火させる」点が特徴だ。JavaScriptのIntersection Observer APIをCSSで再現したような動きになる。

この記事では、両者の違いを比較したうえで、スクロールトリガーアニメーションの基本的な使い方から応用的なテクニックまでを解説する。複数要素の連動やタイムライン範囲の調整など、実践で役立つ手法をまとめた。

スクロールトリガーアニメーションとスクロール駆動アニメーションの違い

スクロールトリガーアニメーションとスクロール駆動アニメーションの違い

まず、両者の動作の違いを明確にしておく。混同しやすい概念だが、設計思想が根本的に異なる。

スクロール駆動アニメーションの基本動作

スクロール駆動アニメーションは、animation-timeline: view()animation-timeline: scroll() を使って、スクロール量や要素の交差度合いにアニメーションの進行を同期させる。スクロールが進むほどアニメーションも進み、戻せばアニメーションも戻る。再生時間という概念はなく、ユーザーのスクロール操作がそのままタイムラインになる。

たとえば、画面下から現れた要素が完全に見えるまでフェードインする、といった表現が得意だ。スクロールに吸い付くような動きで、ユーザーに自然なフィードバックを返せる。

スクロールトリガーアニメーションの基本動作

一方、スクロールトリガーアニメーションは、要素が特定のスクロール位置に達した瞬間にアニメーションを開始する。キーになるのは timeline-trigger: view() プロパティだ。これは「要素がビューポート内にどの程度入っているか」を監視し、指定したしきい値を超えた時点でアニメーションを発火させる。

重要なのは、アニメーションが固定の再生時間を持つことだ。たとえば300msかけて背景色を変える、といった指定ができる。スクロール量に進行が左右されないため、発火後の動きは常に一定になる。これにより、スクロール駆動アニメーションでは実現が難しかった「要素が現れた瞬間に一瞬だけフラッシュさせる」といった演出も容易になる。

スクロール駆動アニメーション
スクロール量 アニメーション進行度
スクロールに比例して0%〜100%まで連続的に変化。時間の概念はない。
スクロールトリガーアニメーション
しきい値到達 アニメーション発火 固定時間で再生
指定したしきい値を超えたら300msなどの固定時間でアニメーションを再生。スクロール量に比例しない。

この概念図では、両者のトリガー条件と進行方法の違いを示している。スクロール駆動は連続的、スクロールトリガーは離散的という理解で問題ない。

基本構文とアニメーションアクション

基本構文とアニメーションアクション

スクロールトリガーアニメーションの最小構成は、@keyframes でアニメーションを定義し、timeline-triggeranimation-trigger で発火条件を指定する、という流れになる。

基本例とプロパティの役割

たとえば、正方形の要素が完全にビューポートに入った瞬間に、背景色が300msかけてフェードインするアニメーションを考える。コードの基本形は以下のようになる。

/* アニメーションの定義 */
@keyframes fade-bg-in {
  to {
    background: currentColor;
  }
}

.square {
  /* アニメーションの宣言 */
  animation: fade-bg-in 300ms;

  /* 発火条件の定義 */
  timeline-trigger: --trigger view() entry 100% exit 0%;

  /* 発火時の動作設定 */
  animation-trigger: --trigger play-forwards;
}

timeline-trigger の値で指定している entry 100% exit 0% はタイムライン範囲と呼ばれる。これは「要素の下端がビューポートに入った瞬間(entry 100%)から、上端がビューポートから出るまで(exit 0%)を発火可能な範囲とする」という意味だ。この範囲内に要素が入っているあいだ、アニメーションの再生が許可される。

animation-trigger に指定した play-forwards は、要素が完全に見えるたびにアニメーションを順方向(0%→100%)で再生するキーワードだ。ただし、このままではアニメーション終了後に背景色が元に戻ってしまう。スタイルを保持するには animation-fill-mode を併用する必要がある。

fill-mode と action の組み合わせ

CSS-Tricksの著者Carlo Daniele氏によると、fill-modeとアニメーションアクションの組み合わせが、スクロールトリガーアニメーションの挙動を大きく左右する。主な選択肢は以下の3パターンだ。

① フラッシュ型(fill-modeなし)
最初のデモのように、アニメーション終了後に元に戻る。一瞬の強調表示に向く。
animation: fade-bg-in 300ms;
② ロックイン型(forwards + play-once)
アニメーションが一度だけ再生され、その後スタイルが保持される。要素が出入りしても再発火しない。
animation: fade-bg-in 300ms forwards;
animation-trigger: --trigger play-once;
③ 往復型(forwards + play-forwards play-backwards)
要素が見えている間は順方向、見えなくなったら逆方向にアニメーション。チラつきなく自然な切り替えが可能。
animation: fade-bg-in 300ms forwards;
animation-trigger: --trigger play-forwards play-backwards;
フラッシュ型  ロックイン型  往復型

往復型の play-forwards play-backwards は特に実用的だ。要素がビューポート外に出る際にアニメーションが逆再生されるため、ちらつきが発生しない。fill-modeを forwards にしていても、最終キーフレームが0%でも100%でも「完了時のスタイルを保持する」というルールが適用されるため、自然に動作する。

アクションキーワード一覧

animation-trigger に指定できるアクションキーワードは以下の表にまとめた。設計の幅を広げるために把握しておきたい。

アクション
効果
play-forwards
アニメーションを順方向(0%→100%)で再生
play-backwards
逆方向(100%→0%)で再生
play-once
順方向または逆方向のどちらか最初の一度だけ再生
play
最後に指定された方向で再生(未指定時は順方向)
pause
アニメーションを一時停止
reset
一時停止し、進行度を0にリセット
replay
進行度を0にリセットするが、一時停止はしない

これらのアクションを組み合わせることで、「スクロールに応じてアニメーションを途中で反転させる」「一度だけ発火させて二度と動かさない」といった細かな制御が可能になる。

複数要素への適用とスタッガー効果

複数要素への適用とスタッガー効果

スクロールトリガーアニメーションの真価は、複数要素を連動させた演出で発揮される。アクションやfill-modeが独立しているため、共通のトリガー設定を使い回しながら個別のアニメーションを割り当てられる。

CSSカスタムプロパティで設定を再利用する

たとえば、3つの正方形が順番に回転しながら拡大・背景変化する例を考える。各要素に個別のスタイルを書くのではなく、カスタムプロパティで共通設定をまとめると保守性が高まる。

.square {
  scale: 70%;
  --base-animation: intensify;
  animation: var(--base-animation) 300ms forwards;
  --animation-trigger: --trigger play-forwards play-backwards;
  animation-trigger: var(--animation-trigger), var(--animation-trigger);
  timeline-trigger: --trigger view();
  timeline-trigger-active-range-end: normal;
}

.square.rotate-left {
  animation-name: var(--base-animation), rotate-left;
}

.square.rotate-right {
  animation-name: var(--base-animation), rotate-right;
}

このコードでは、--animation-triggerplay-forwards play-backwards を格納し、animation-trigger でカンマ区切りによって2つのアニメーションに同じトリガー設定を適用している。これにより、メインのアニメーション(intensify)と回転アニメーションの両方が同じタイミングで発火する。

sibling-index() と sibling-count() による自動スタッガー

スクロールトリガーアニメーションでは、sibling-index()sibling-count() という2つのCSS関数を使って、兄弟要素のインデックスと数を動的に取得できる。これを利用すると、各要素の発火タイミングを自動的にずらす「スタッガー」が実装可能だ。

.square {
  --stagger-interval: calc(100% / sibling-count());
  --entry: calc(sibling-index() * var(--stagger-interval));
  timeline-trigger: --trigger view() entry var(--entry) exit 0%;
}

sibling-count() で親要素内の.squareの数を取得し、それを100%で割ることで1要素あたりの間隔を算出する。次に sibling-index() で現在の要素が何番目かを取得し、間隔を掛け算して各要素の entry 値を動的に決定する。この仕組みがあれば、HTMLの要素数が変わっても手作業で値を調整する必要はない。

ただし、sibling-index()sibling-count() はFirefoxで未サポートの点に注意が必要だ。プロダクションで使う場合は、フォールバックの指定を検討したい。

特定の要素をトリガーとして他を連動させる

さらに高度な使い方として、最初の要素だけをトリガーにして、残りの要素はアニメーション遅延で続けて発火させる方法がある。トリガー条件を1箇所に集約できるため、管理がしやすくなる。

.square:first-child {
  timeline-trigger: --trigger view() entry 50%;
  timeline-trigger-active-range-end: normal;
}

.square {
  --stagger-interval: calc(300ms / sibling-count());
  --animation-delay: calc(sibling-index() * var(--stagger-interval));
  animation: var(--base-animation) 300ms var(--animation-delay) forwards;
}

最初の要素がビューポートに50%入った時点でトリガーが発火し、残りの要素は animation-delay で順番にアニメーションを開始する。この方法なら、トリガー設定は最初の要素にだけ書けばよい。

ただし、CSS-Tricksの記事では play-backwards 状態のときに遅延が意図通り動作しない可能性が指摘されている。逆再生時にはdelayが含まれてしまい、スタッガーが崩れるようだ。今後のブラウザ実装の改善が待たれる。

タイムライン範囲の理解とカスタマイズ

タイムライン範囲の理解とカスタマイズ

スクロールトリガーアニメーションには、アクティベーション範囲(発火可能な範囲)とアクティブ範囲(発火後に有効であり続ける範囲)という2つのタイムライン範囲が存在する。この概念を理解しておくと、より精密なトリガー制御が可能になる。

アクティベーション範囲とアクティブ範囲の違い

アクティベーション範囲は「アニメーションが発火できるスクロール区間」を定義する。例えば entry 100% exit 0% は、要素が完全に見えてから完全に隠れるまでを発火可能区間とする。アクティブ範囲は「一度発火したアニメーションが、たとえアクティベーション範囲を外れても有効であり続ける区間」を指す。

アクティベーション範囲(発火可能区間)
アニメーションのトリガーが許可されるスクロール範囲。
例: entry 100% exit 0% は、要素の下端が入ってから上端が出るまで。
アクティブ範囲(有効区間)
アニメーションが発火したあとに、その状態が保持されるスクロール範囲。
timeline-trigger-active-range-end: normal; で制御可能。
発火可能区間  有効区間

実際のところ、大半のユースケースでは view() entry 100% exit 0%(要素が完全に見える間)か view() contain(要素がビューポートより大きい場合も含めて交差している間)で十分だ。タイムライン範囲の詳細な制御が必要になるのは、特殊な演出や複雑なレイアウトに限られる。

view() 関数とオフセットの調整

view() 関数はビューポートを基準にしたタイムライン範囲を定義する関数だ。固定ヘッダーなどでビューポートの実質的な領域が狭まっている場合、view(y 0 5rem) のようにオフセットを指定できる。これにより、ヘッダーに隠れる部分を除いた「実際に見えている領域」を基準にできる。

たとえば、上部に高さ5remの固定ヘッダーがあるページでは、view(y 0 5rem) と書くことで、ヘッダーの裏に入った要素はまだトリガー範囲内と見なされない。この調整は、UIコンポーネントとスクロール演出を正確に同期させる際に役立つ。

この記事のポイント

  • スクロールトリガーアニメーションは、スクロール駆動アニメーションと異なり、固定時間のアニメーションを特定のスクロール地点で発火させる仕組みである
  • fill-modeとanimation-actionの組み合わせで、往復・ロックイン・フラッシュなど多彩な演出が可能
  • sibling-index()とsibling-count()を使えば、要素数に依存しない自動スタッガーを実装できる
  • 実用上はview() entry 100% exit 0%の指定で十分だが、固定ヘッダーがある場合はview()のオフセット調整を検討する
スクロール駆動アニメーションとスクロール状態、ビュー遷移の違いを整理

スクロール駆動アニメーションとスクロール状態、ビュー遷移の違いを整理

CSSのスクロール系アニメーションは混同しやすい概念が多い。スクロール駆動アニメーション、スクロールトリガーアニメーション、コンテナクエリのスクロール状態、そしてビュー遷移。いずれも「動き」を伴う機能だが、仕組みは大きく異なる。それぞれの動作を整理し、いつどの技術を使うべきかを明確にする。

CSS-Tricksの著者Geoff Graham氏も自身の記事で「言いたいことと意味することが違っていたり、必要な場面で別のものを選んでしまったりする」と述べており、開発者であれば誰もが直面する混乱だ。この記事では、4つの技術の本質的な違いをデモとともに確認していく。

スクロール駆動アニメーションとは

スクロール駆動アニメーションとは

スクロール駆動アニメーション(Scroll-Driven Animations)は、スクロールの進行状況とアニメーションの進行が直接連動する仕組みだ。ユーザーがスクロールバーを動かすとアニメーションが同じ比率で動き、スクロールを止めればアニメーションも止まる。逆方向にスクロールすれば、アニメーションも逆再生される。

これはつまり、スクロール位置がアニメーションのタイムラインを完全にコントロールすることを意味する。アニメーションの各フレームがスクロールの各ピクセルに紐づいているイメージだ。

スクロール駆動アニメーションの動作モデル
スクロール位置とアニメーション進行の関係
スクロール 0% → アニメーションも 0% 進行
スクロール 50% → アニメーションも 50% 進行
スクロール 100% → アニメーションも 100% 完了
双方向リンク:逆スクロールで逆再生

このデモで示すように、スクロール駆動アニメーションはスクロールという入力装置がそのままアニメーションの進行度を決める。ユーザーが任意の位置で停止できるし、前後にも動かせる。パララックス効果やプログレスバーなど、スクロールに完全に同期した視覚効果を作るのに向いている。

基本的なCSSの書き方

.element {
  animation: grow-progress linear forwards;
  animation-timeline: scroll();
}

animation-timeline: scroll() がキモだ。通常のアニメーションは時間ベースのタイムラインで動くが、これをscroll()にすることで、スクロール位置をタイムラインとして使えるようになる。これにより、スクロール量に応じて要素の透明度・サイズ・位置などを段階的に変化させられる。

スクロールトリガーアニメーションとは

スクロールトリガーアニメーションとは

スクロールトリガーアニメーション(Scroll-Triggered Animations)は、スクロール駆動と名前が似ているが、動作原理はまったく異なる。こちらはスクロール位置とアニメーション進行が連動しない。ある要素が特定のしきい値(トリガー活性化範囲)を越えた瞬間にアニメーションが発火し、最後まで一気に実行される。

スクロール駆動 vs スクロールトリガーの動作比較
スクロール駆動(Before)
スクロール量に比例 → 連続的に変化
※スクロール停止でアニメーションも停止
スクロールトリガー(After)
しきい値で発火 → 最後まで一気に実行
※発火後はスクロールに影響されない

トリガーは要素がスクロールポートに「入った」「出た」といったイベントだ。アニメーションが走り出してしまえば、その後のスクロール操作は関係ない。スクロール駆動アニメーションが「巻き戻せるビデオテープ」だとすれば、スクロールトリガーアニメーションは「一度押したら最後まで流れる再生ボタン」だ。

どんな場面で使うか

画面に要素が現れた瞬間にフェードインさせたり、スライドインさせたりするUI演出が代表的な用途だ。一度表示された後にスクロールで逆再生する必要がなければ、トリガー方式のほうがシンプルで意図が明確になる。逆にパララックスやプログレスインジケーターのように常時追従が必要な場合は、スクロール駆動を使う。

コンテナクエリのスクロール状態

コンテナクエリのスクロール状態

コンテナクエリのスクロール状態(Container Query Scroll State)は、CSS Conditional Rules Module Level 5のワーキングドラフトに含まれる比較的新しい概念だ。一言でいうと、コンテナが特定のスクロール条件に達したときにスタイルを更新する仕組みである。

スクロール駆動とスクロールトリガーの中間のような動きをする。スクロール位置に応じて何かが変わる点では駆動に近いが、アニメーションではなく「状態の切り替え」を扱う点が異なる。

.sticky-nav {
  container-type: scroll-state;
  position: sticky;
  top: 0;

  @container scroll-state(stuck: top) {
    background: orangered;
    border-radius: 0;
    flex-direction: row;
    width: 100%;

    a {
      text-decoration: none;
    }
  }
}
scroll-state の動作イメージ
通常時(sticky未発動)
ロゴ
メニュー 1(下線あり)
メニュー 2(下線あり)
縦並び・角丸・リンク下線あり
スクロールで上部固定時(stuck: top)
ロゴ
メニュー 1
メニュー 2
横並び・角丸ゼロ・背景色変更・リンク下線消去

この例では、position: stickyのナビゲーションがビューポート上部に張り付いた瞬間にレイアウトや配色が切り替わる。JavaScriptなしでスクロールに応じた状態管理をCSSだけで行える点が強力だ。ただし、2026年6月時点ではまだワーキングドラフト段階であり、ブラウザの対応状況を確認してから使う必要がある。

ビュー遷移(View Transitions)

ビュー遷移(View Transitions)

ビュー遷移は、ここまでの3つとは根本的に異なる。スクロールとは関係がない。CSSとJavaScriptが連携する完全なAPIで、ページ遷移や同一ページ内の状態変化をアニメーションさせる仕組みである。

同一ドキュメント遷移

同一ドキュメント遷移(Same-document transitions)は、ユーザー操作によって同一ページ上の要素がある状態から別の状態へ変化する際に使う。たとえばラジオボタンの選択状態が別の項目に移動するアニメーションや、グリッドビューからリストビューへの切り替えアニメーションなどが該当する。

クロスドキュメント遷移

クロスドキュメント遷移(Cross-document transitions)は、ページAからページBへ移動する際のアニメーションを実現する。デフォルトではクロスフェード(ページAが徐々に消え、ページBが徐々に現れる)で、実装も比較的簡単だ。複雑なエフェクトも可能で、たとえば円形のclip-pathで最初のページをワイプアウトしながら次のページを表示する、といった演出もできる。

クロスドキュメント遷移の流れ
遷移前(ページA)
🅰️ ページAのコンテンツ
遷移中(クロスフェード)
🅰️ 消えゆくページA → 🅱️ 現れつつあるページB
遷移後(ページB)
🅱️ ページBのコンテンツ

ビュー遷移の大きな利点は、MPA(マルチページアプリケーション)でもSPA(シングルページアプリケーション)のようなスムーズなページ遷移を実現できることだ。ChromeチームのBramus氏が多数の美しいデモを公開しており、実践的な実装例も増えている。

4つの技術をどう使い分けるか

4つの技術をどう使い分けるか

ここまでの整理を踏まえて、実務での使い分けを考える。選択の基準は「スクロールとアニメーションがどう関係するか」に尽きる。

4技術の目的別マッピング
パララックス・プログレスバー → スクロール駆動アニメーション
要素出現時のフェードイン → スクロールトリガーアニメーション
固定ヘッダーの状態切替 → コンテナクエリ スクロール状態
ページ遷移アニメーション → ビュー遷移

スクロール駆動はスクロール量に完全同期させたい場合、スクロールトリガーは「画面に入ったらポン」と動かしたい場合、スクロール状態は「ある条件下で見た目をパッと切り替えたい」場合、ビュー遷移は「ページ間や状態間をなめらかにつなぎたい」場合に選ぶ。

技術選定でよくある失敗は、スクロール駆動で十分な場面でトリガーを使ってしまったり、逆にトリガーで済む場面で複雑な駆動ロジックを書いてしまうことだ。要件に対してシンプルなほうを選ぶのが鉄則である。また、ビュー遷移をスクロール系の技術と混同しないことも重要だ。ビュー遷移はスクロールとは独立したレイヤーで動作するため、併用も可能だが別物として扱う必要がある。

この記事のポイント

  • スクロール駆動アニメーションはスクロール位置とアニメーション進行が双方向に連動する
  • スクロールトリガーアニメーションはしきい値で発火し、最後まで一気に実行される
  • コンテナクエリのスクロール状態は特定のスクロール条件でスタイルを切り替える(ドラフト段階)
  • ビュー遷移はスクロールとは無関係で、ページ遷移や状態変化をアニメーションさせるAPI
  • 実装前に「スクロールとどう関係するか」を明確にし、最もシンプルな技術を選ぶ
CSSのletter-spacingでテキスト表示を切り替える実装テクニック

CSSのletter-spacingでテキスト表示を切り替える実装テクニック

CSSでテキストを一文字ずつ表示したり、特定の単語を切り替えたりする演出は、直感的には難しいものだ。::nth-letter()のような仮想的なセレクタがあれば楽だが、現状のCSS仕様には存在しない。しかし、letter-spacingプロパティの負の値とcolor: transparentを組み合わせることで、限定的ながらも文字単位の表示制御が実現できる。

CSS-Tricksの記事では、このテクニックを使ってチェックボックス操作によるラベル切り替えや、アクロニムの全文表示といった実装例が紹介されている。本記事ではその仕組みと実装手順を掘り下げ、日本国内のWeb制作現場での活用ポイントを考察する。

letter-spacingの基本と隠しテキストの仕組み

letter-spacingの基本と隠しテキストの仕組み

正の値と負の値の効果

letter-spacingプロパティは、各文字の右側に追加されるスペースを調整する。正の値では文字間隔が広がり、負の値ではグリフボックスの幅が縮まる。値を十分に小さく設定すると、隣り合う文字同士が重なり合い、最終的には一箇所に集約された状態になる。

この状態でテキストの色をtransparentに指定すれば、ユーザーからは完全に見えなくなる。逆に正の値に戻すと文字が再び分離して表示される。この性質をアニメーションと組み合わせることで、表示と非表示を切り替える演出が可能になる。

負のletter-spacingで重なる文字
サンプルテキスト
letter-spacing:-0.5ch の状態。文字同士が接近し、重なりが生じている
正の値に戻した読みやすい状態
サンプルテキスト
letter-spacing:0ch (デフォルト)。文字が分離し可読性が戻っている

上記のデモでは、負の値によって文字が詰まったビジュアルと、正の値(0)で通常表示に戻る様子を並べている。実際にはtransitionプロパティを加えることで、この変化をなめらかに動かせる。

ch単位を使う理由

文字の重なり具合を指定する負の値には、ch単位が特に相性が良い。1chは数字の「0」のグリフ幅に相当する相対単位であり、フォントファミリーやサイズに応じて自動調整される。これにより、使用する書体が変わっても一貫した重なり効果を維持しやすくなる。

例えばletter-spacing: -1chを指定すると、各文字が1文字分ずつ左に詰められ、理論上は完全に重なる。実際にはフォントのデザインやカーニングによって微妙なズレが生じることもあるが、調整の起点として扱いやすい。

チェックボックスと組み合わせたテキスト切り替え

チェックボックスと組み合わせたテキスト切り替え

HTMLとCSSのコード例

このテクニックを応用すると、チェックボックスの状態に応じてラベルテキストを動的に切り替えるUIを作成できる。以下は、クリックによって「入会する」のような案内文が「ようこそ」メッセージに変化するパターンだ。

input:checked + label .initial-text {
  letter-spacing: -2ch;
  text-indent: -1.5ch;
  transition: 0.4s letter-spacing cubic-bezier(.8, -.5, .2, 1.4), 
              0.1s text-indent;
}

input:checked + label .revealed-text {
  letter-spacing: 0ch;
  color: #1a1a2e;
  transition:
    0.4s letter-spacing cubic-bezier(.8, -.5, .2, 1.4) 0.3s, 
    0.8s color 0.4s;
}
非チェック状態(Before)
会員登録して特典を受け取る
登録完了しました
案内テキストが表示され、完了メッセージは letter-spacing で潰されて非表示
チェック状態(After)
会員登録して特典を受け取る
登録完了しました
チェックすると案内テキストが letter-spacing で潰れて消え、完了メッセージが表示される

デモではoverflow: clipが適用されたコンテナ内で、一方のテキストがletter-spacing: -2chtext-indentで左に押し出され、もう一方が通常の間隔に戻る仕組みだ。実際の環境ではチェックボックス操作によりこれらのプロパティが切り替わる。

アニメーションの調整ポイント

CSS-Tricksの著者Carlo Daniele氏の実装例では、cubic-bezier(.8, -.5, .2, 1.4)というイージングが使われている。この曲線は、変化の途中で値が目標値を超えて戻る「バウンス」効果を生み出し、文字が勢いよく離れるような動きを演出する。

また、2つのテキストのtransition-delayをずらすことで、古いテキストが消え始めてから新しいテキストが現れるまでの間に自然なオーバーラップが作られている。この遅延調整は、ユーザーが違和感なく情報の切り替わりを認識できるようにするための工夫だ。

アクロニムの全文表示テクニック

アクロニムの全文表示テクニック

::first-letterの活用

UNICEF(United Nations International Children’s Emergency Fund)のようなアクロニムを題材に、各単語の最初の文字だけを常に表示し、ホバー時に残りの文字を出現させるテクニックが紹介されている。

.acronym-word {
  letter-spacing: -1ch;
  color: transparent;
}

.acronym-word::first-letter {
  color: #1a1a2e;
}

figure:hover + .acronym .acronym-word {
  letter-spacing: 0ch;
  color: #1a1a2e;
  transition: letter-spacing 0.4s cubic-bezier(.8, -.5, .2, 1.4);
}
未ホバー時(Before)
United Nations International Children’s Emergency Fund
U N I C E F
各単語の頭文字(U N I C E F)だけが表示されている
ホバー時(After)
United Nations International Children’s Emergency Fund
完全な単語が展開され、アクロニムの正式名称が読める

::first-letter疑似要素で頭文字だけを黒く表示し、残りの文字はcolor: transparentで不可視にしておく。ホバーイベントでletter-spacingを0に戻すと、すべての文字が可視状態で展開される仕組みだ。

実装の注意点

このパターンでは、各単語を個別の<span>で囲む必要がある。単一のテキストブロックに対して::first-letterは最初の1文字にしか適用されないからだ。UNICEFの例のように6つの単語があれば、6つの要素でマークアップすることになる。

また、スクリーンリーダーはcolor: transparentのテキストも読み上げるため、アクセシビリティ面では注意が必要だ。このテクニックはあくまでビジュアル面の演出であり、情報の一次的な伝達手段としては適さない。重要なテキストはaria-labelなどで別途提供するか、この効果を装飾的な目的に限定するのが安全だ。

実務での活用アイデア

実務での活用アイデア

このテクニックは、以下のようなシーンで効果を発揮する。

申し込みボタンのラベル切り替え
「送信する」から「送信完了」へ、ボタン内のテキストをスムーズに変化させる。ユーザーの操作完了を直感的に伝えられる。
用語集やFAQのアクロニム展開
「SEO」にカーソルを合わせると「Search Engine Optimization」が表示される仕掛け。補足情報を必要なときだけ見せられる。
ナビゲーションの状態表示
現在位置を示すテキストを他の項目と視覚的に区別し、アクティブ項目を強調する効果として使える。

いずれも、派手なアニメーションライブラリを使わずにCSSだけで完結するのが利点だ。パフォーマンス面でもJavaScriptによるDOM操作より軽量で、メインスレッドへの負荷が少ない。

制約と対応ブラウザ

制約と対応ブラウザ

letter-spacingのアニメーションは主要なモダンブラウザで広くサポートされている。ただし、cubic-bezierによるバウンス効果は、イージングの値によっては環境間で微妙な見え方の差が出ることがある。

また、ch単位はフォントの「0」の幅に依存するため、和文フォントと欧文フォントが混在する日本語サイトでは、想定よりも文字の重なり方が異なるケースがある。実装時は実際のフォントスタックで表示確認を行うことが重要だ。

この記事のポイント

  • letter-spacingの負値とcolor: transparentを組み合わせると、文字を一箇所に重ねて不可視にできる
  • チェックボックスの状態に応じたラベル切り替えは、transition-delayの調整で自然なUI演出になる
  • アクロニムの全文表示には::first-letterと単語ごとの<span>分割が必要
  • アクセシビリティに配慮し、重要な情報は視覚効果だけに依存しない設計が求められる
クロスドキュメントビュー遷移の三大落とし穴と回避策

クロスドキュメントビュー遷移の三大落とし穴と回避策

クロスドキュメントビュー遷移(Cross-Document View Transitions)は、MPA(マルチページアプリケーション)でありながらSPAのようなスムーズなページ遷移アニメーションを実現するブラウザAPIだ。ReactやAstroといったフレームワークは不要で、HTMLページ間のリンク遷移にブラウザが自動的にアニメーションを付与する。

しかし、この機能の導入にはいくつかの厄介な落とし穴が潜んでいる。CSS-Tricksの記事によれば、著者は実装に丸一日を費やし、何も動作しない状態からデバッグを繰り返したという。ネット上には古い情報や誤解を招くチュートリアルが溢れており、仕様自体も短期間で変更されている。

本記事では、実際の開発現場で遭遇する3つの主要な問題(非推奨metaタグ、4秒タイムアウト、画像の歪み)とその解決策を解説する。加えて、遷移ライフサイクルを制御する2つのイベントについても触れる。

非推奨となったmetaタグの罠

非推奨となったmetaタグの罠

多くの開発者が最初にハマるのが、古いチュートリアルに記載された<meta>タグによるオプトイン方式だ。この方式は既に非推奨であり、現在のブラウザでは完全に無視される。

非推奨(Before)
<meta name=”view-transition” content=”same-origin”>
ブラウザが沈黙し、何も動作しない
推奨(After)
@view-transition { navigation: auto; }
CSSでオプトイン、条件付き制御も可能

この比較が示すように、現在の正しい実装方法はCSSの@規則を使用することだ。Chrome 111でmetaタグが導入された後、Chrome 126前後でCSSベースの方式に置き換えられた。非推奨の警告はDevToolsに表示されず、古いコードは静かに動作しなくなる。

なぜCSS方式に移行したのか

metaタグ方式の最大の欠点は、ページ全体でオンかオフかの二択しかできなかったことだ。CSS方式ではメディアクエリや@supportsと組み合わせて、条件付きのオプトインが可能になる。

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

@media (min-width: 768px) {
  @view-transition {
    navigation: auto;
  }
}

このアプローチにより、アニメーションに敏感なユーザーへの配慮や、モバイルデバイスでのパフォーマンス最適化が容易になった。CSSにオプトインが統合されたことで、既存のスタイル管理フローと一貫性を持って扱える点も大きい。

両方のページでオプトインが必須

もう一つの重要なポイントは、遷移を機能させるには遷移元と遷移先の両方のページで@view-transitionが宣言されている必要があることだ。片方だけでは何も起きない。これは意図的な設計で、404ページやログインリダイレクトなど遷移をスキップしたいページを柔軟に制御できる。

なお、navigation: autoはユーザーがリンクをクリックするかブラウザの戻るボタンを押した場合のみ発動する。window.location.hrefによるプログラム的な遷移や、クロスオリジンのリンク、POSTリクエストでは動作しない。この保守的な設計は、決済処理などの重要な操作に意図しないアニメーションが混入するのを防ぐためだ。

4秒タイムアウトが遷移を静かに殺す

4秒タイムアウトが遷移を静かに殺す

ビュー遷移の実装で最もデバッグが難しい問題が、ハードコードされた4秒のタイムアウトだ。新しいページが4秒以内にレンダリング可能な状態に達しないと、遷移アニメーションは何の通知もなくキャンセルされ、通常のページ読み込みのように切り替わる。

タイムアウト発生 4秒経過 ブラウザ 遷移を破棄 エラー表示なし
※TTFB(サーバー応答待ち2秒 + レンダリング2.5秒でアウト)

この問題が厄介なのは、ローカル開発環境ではまず発生しないことだ。devサーバーは80msで応答するため遷移は完璧に動作するが、本番環境でサーバーレス関数のコールドスタートやCDNキャッシュミスが発生すると、最初のクリックで遷移が無効化される。

pagerevealイベントでタイムアウトを捕捉する

タイムアウトの発生を検知するには、pagerevealイベントとviewTransition.finishedプロミスを使用する。以下のコードをページに組み込めば、遷移が失敗した際にコンソールで確認できる。

window.addEventListener("pagereveal", (event) => {
  if (!event.viewTransition) {
    console.log("ビュー遷移なし");
    return;
  }
  event.viewTransition.finished
    .then(() => console.log("遷移完了 ✅"))
    .catch((err) => {
      console.error("遷移中断", err.name, err.message);
    });
});

このリスナーを早期にセットアップしておけば、本番環境でのデバッグが格段に容易になる。pageswapイベントでも同様に遷移元ページ側でタイムアウトを捕捉可能だ。

実用的な対策

タイムアウト対策の基本はページの読み込み速度改善だが、より実践的なアプローチとしてrel="expect"属性の活用がある。

<link rel="expect" href="#hero" blocking="render">

これはブラウザに「#hero要素がDOMに存在するまでページをレンダリング可能と見なさない」と指示するものだ。一見するとパフォーマンスを悪化させるように思えるが、ビュー遷移においては重要なコンテンツが揃ってからスナップショットを取得するため、中途半端な状態での遷移を防げる。

タイムアウトのクロックはナビゲーション開始時からカウントされるため、サーバー応答時間(TTFB)も含まれる点に注意が必要だ。サーバーが2秒かけて応答し、さらに2.5秒かけてレンダリングする場合、個別には遅く感じなくても合計で4.5秒となりタイムアウトに引っかかる。

画像が歪む根本原因と解決策

画像が歪む根本原因と解決策

ビュー遷移の実装で視覚的に最も目立つ問題が、アスペクト比の異なる画像間の遷移で発生する歪みだ。サムネイルからヒーロー画像への遷移で、画像が引き伸ばされて見苦しくなる現象は多くの開発者が経験する。

CSS-Tricksの著者は、この問題の原因を特定するのにかなりの時間を費やしたという。根本的な原因は、ブラウザが遷移中に<img>要素そのものをアニメーションさせるのではなく、古い状態と新しい状態のスクリーンショット(ビットマップ)を取得し、それらを変形させることにある。

変形した状態(Before)
サムネイル
object-fit: cover → 無効化
ビットマップが引き伸ばされる
整形された状態(After)
::view-transition-old(hero-img),
::view-transition-new(hero-img) {
object-fit: cover;
overflow: hidden;
}

上記の図が示すように、解決策は疑似要素::view-transition-old::view-transition-newに対してobject-fit: coverを適用することだ。これにより、スナップショット画像がアスペクト比を維持したまま切り抜かれるようになる。

疑似要素ツリーの構造を理解する

ビュー遷移が発生すると、ブラウザは内部的に次のような疑似要素ツリーを生成する。

::view-transition
└── ::view-transition-group(hero-img)
    ├── ::view-transition-old(hero-img)
    └── ::view-transition-new(hero-img)

::view-transition-groupが古い寸法から新しい寸法へアニメーションするコンテナとして機能し、その中のoldnewが実際のスナップショットを保持する。デフォルトではこれらの疑似要素にobject-fit: fillが適用されており、これが歪みの原因となる。

アスペクト比が大きく異なるケースでは、object-positionで切り抜き位置を調整することも有効だ。

::view-transition-old(hero-img) {
  object-fit: cover;
  object-position: center center;
}
::view-transition-new(hero-img) {
  object-fit: cover;
  object-position: center top;
}

このコードでは、新しいヒーロー画像の上部を優先的に表示しつつ、遷移中の歪みを防ぐことができる。CSS-Tricksの著者も指摘するように、object-fit: coverはほぼ全ての画像遷移で必要になる設定であり、デフォルトがfillであることは実用上の大きな障壁となっている。

pageswapとpagerevealによるライフサイクル制御

pageswapとpagerevealによるライフサイクル制御

クロスドキュメントビュー遷移では、遷移元と遷移先のページがJavaScriptで直接通信できないという制約がある。この問題を解決するのがpageswappagerevealの2つのイベントだ。

遷移元ページ pageswap発火 要素に名前を付与
event.activation.entry.url で遷移先を特定可能
遷移先ページ pagereveal発火 遷移元の情報を取得
navigation.activation.from.url で遷移元を特定可能

このイベントペアにより、開発者は遷移の両端で状態を制御できる。pageswapは遷移元ページがスナップショットされる直前に発火し、event.activation.entry.urlでユーザーがどこへ向かっているかを知ることができる。

イベントハンドラの実装パターン

これらのイベントを使用する際の重要なポイントは、必ずevent.viewTransitionの存在確認を行うことだ。pagerevealはビュー遷移がない場合も含め、全てのナビゲーションで発火する。

window.addEventListener("pagereveal", (event) => {
  if (!event.viewTransition) return;
  
  event.viewTransition.finished.then(() => {
    // 遷移完了後のクリーンアップ
  }).catch((err) => {
    // タイムアウト等のエラー処理
  });
});

CSS-Tricksの記事では、商品一覧ページから商品詳細ページへの遷移において、pageswapでクリックされた商品カードだけにview-transition-nameを動的に付与するパターンが紹介されている。この動的な名前付けは、数十から数百の要素があるページでのスケーラビリティ問題を解決する重要な手法だ。

この記事のポイント

  • metaタグ方式は非推奨。CSSの@view-transition { navigation: auto; }を使用する
  • 4秒のタイムアウトはTTFBを含む総時間で判定され、pagerevealイベントで捕捉可能
  • 画像歪みは疑似要素::view-transition-old/newへのobject-fit: cover適用で解決
  • pageswappagerevealの2つのイベントが遷移全体のライフサイクルを制御する
CSSだけでApple Vision Pro風スクロールアニメーションを再現する高度なテクニック

CSSだけでApple Vision Pro風スクロールアニメーションを再現する高度なテクニック

Appleの製品ページで多用される、スクロールに連動したダイナミックなアニメーションは、多くのWeb制作者にインスピレーションを与えてきた。特にVision Proの紹介ページで見られる、デバイスが分解されながら迫ってくるような演出は、技術的にも非常に洗練されている。

これまでこうした演出の多くはJavaScriptを用いて制御されていたが、最新のCSS機能を駆使することで、スクリプトなしでの再現が可能になりつつある。CSS-Tricksの記事では、スクロール駆動アニメーション(Scroll-driven Animations)を活用し、Apple風の演出をCSSだけで構築する手法が提案された。

本記事では、その実装の核心となる「パーツの分解」と「デバイスの反転」という2つのステージを、最新のCSSプロパティを用いてどのように制御するのかを深掘りしていく。パフォーマンスとレスポンシブ対応を両立させるための、具体的な計算式や構造の設計についても詳しく見ていこう。

Appleのスクロール演出を構成する2つのステージ

Appleのスクロール演出を構成する2つのステージ

Vision Proのアニメーションを再現するためには、まずその動きを論理的に分解する必要がある。CSS-Tricksの分析によれば、この演出は大きく分けて2つの段階で構成されているという。

ステージ1 ハードウェアの分解表示

最初の段階では、デバイスの底部から3つの主要な電子部品が順番に浮き上がってくる。それぞれのコンポーネントは、他の部品を挟み込むように配置された2枚の画像で構成されている。これにより、部品が重なり合いながらも奥行きを感じさせる、立体的な「爆発図」のような効果が生まれる。

この視覚効果のポイントは、透明な領域を含む複数のレイヤーが、スクロールに合わせて異なる速度やタイミングで移動することだ。最前面と最後面に配置された画像が、中間にある部品を包み込むように動くことで、単なる平面の移動ではない3D的な深みが表現されている。

ステージ2 接眼レンズへのフリップアップ

部品の分解が終わると、次にデバイス全体が滑らかに回転し、接眼レンズ(アイピース)が見える状態へと変化する。Appleの公式サイトでは、この部分はJavaScriptで動画の再生位置をスクロール量に合わせて制御することで実現されている。

これをCSSだけで再現する場合、動画ファイルの代わりに大量の静止画を高速で切り替える手法が検討される。スクロールというユーザーの入力に対して、パラパラ漫画のように画像を差し替えていくことで、動画と同等の滑らかな回転アニメーションを作り出すアプローチだ。

Gridレイアウトによる要素の重ね合わせと配置

Gridレイアウトによる要素の重ね合わせと配置

アニメーションを実装する前の準備として、複数の部品画像を正確に重ね合わせる必要がある。従来は position: absolute を多用していたが、これでは要素が通常の文書フローから外れてしまい、レスポンシブ対応やスクロール位置の管理が複雑になるという課題があった。

CSS-Tricksの筆者は、この問題の解決策として display: grid の活用を挙げている。親要素を1カラム・1行のグリッドに設定し、すべての部品画像を同じグリッドエリア(grid-area: 1 / 1 / 2 / 2)に割り当てることで、文書フローを維持したまま完璧な重ね合わせを実現できる。

Gridによる重ね合わせの概念図
背面パーツ
中央パーツ
前面パーツ
各要素が同じエリアに重なりつつ、個別に移動可能

このデモのように、グリッドを使うことで要素の順序(z-index)を保ちながら、個々のパーツに自由なアニメーションを適用できる土台が整う。また、各画像に background-size: cover を適用することで、アスペクト比を維持したまま画面幅に合わせることも容易になる。

StickyとView Timelineによるスクロール制御

StickyとView Timelineによるスクロール制御

スクロールに応じてアニメーションを動かす際、最も重要なのが「要素が画面内の特定の場所に留まり続けること」と「要素の表示状態を検知すること」の2点だ。これを実現するのが position: stickyview-timeline プロパティである。

要素を画面に固定するStickyの役割

アニメーションが実行されている間、対象のデバイスが画面外に流れていってしまっては意味がない。そこで、アニメーション全体を包むコンテナ要素に十分な高さを設定し、中のデバイス要素に position: sticky; top: 0; を指定する。これにより、ユーザーがスクロールしている間、デバイスは画面上部に固定され、アニメーションの変化だけが視覚的に伝わるようになる。

View Timelineによる実行タイミングの最適化

従来、スクロールアニメーションの開始位置を特定するには、JavaScriptでスクロール量を監視し、要素のオフセットを計算する必要があった。しかし、最新のCSSでは view-timeline-name を定義するだけで、その要素がビューポート(画面)に入ってきたことをトリガーにアニメーションを開始できる。

CSS-Tricksの記事では、scroll-timeline ではなく view-timeline を選択した理由として、レスポンシブ性の向上を挙げている。ページの総高さに依存する scroll-timeline よりも、要素自体の表示状態に基づく view-timeline の方が、画面サイズが変わっても正確なタイミングでアニメーションを開始できるからだ。

レスポンシブ対応のための動的な高さ計算

レスポンシブ対応のための動的な高さ計算

アニメーションの移動量を固定値(px)で指定すると、画面サイズが小さいデバイスではパーツが画面外に飛び出したり、逆に移動が足りなかったりする問題が発生する。これを防ぐために、数学的なアプローチが必要となる。

デバイスの画像サイズが 960px × 608px である場合、現在の表示幅に基づいた動的な高さを calc() 関数で算出できる。具体的には、以下の計算式を用いることで、画像の比率を維持した高さを取得し、それを移動量の基準にする手法だ。

:root {
  --stage2-height: calc(min(100vw, 960px) * 608 / 960);
}

@media screen and (max-height: 608px) {
  :root {
    --stage2-height: 100vh;
  }
}

この計算式により、ブラウザの幅が狭いときは 100vw に基づいた高さが計算され、画面の高さが極端に低い場合は 100vh が優先される。こうして得られた --stage2-height 変数を translate プロパティに適用することで、どのような画面サイズでもパーツが適切な位置まで移動し、重なりを維持できるようになる。

パラパラ漫画方式による「動画風」アニメーション

パラパラ漫画方式による「動画風」アニメーション

前述の通り、CSSだけで動画のフレームを制御することはできない。そこで、ステージ2のフリップアップ演出では、背景画像を高速で切り替える手法が採用された。これは、キーフレームアニメーションの中で background-image を順番に指定していく方法だ。

具体的には、0%から100%までの進行度に合わせて、数十枚の静止画(00011.jpg、00013.jpg…)を切り替えていく。この際、パフォーマンスを向上させるために、HTMLの <link rel="preload" as="image"> タグを使用して、すべての画像を事前に読み込んでおくことが推奨されている。これにより、スクロール時の画像のチラつきや遅延を防ぐことができる。

画像切り替えによる回転演出(Before / After)
スクロール開始(画像001)
スクロール中(画像060:回転完了)
● ●
※スクロール位置に応じて背景画像が差し替わり、擬似的に3D回転を表現する

この手法のデメリットは、1つの動画ファイルの代わりに大量の静止画をダウンロードする必要がある点だ。CSS-Tricksの筆者は、フレーム数を半分に間引くことでファイル数を削減しつつ、視覚的な滑らかさを維持する工夫を凝らしている。実運用では、画像の最適化やスプライト画像化などのさらなる対策が有効だろう。

Animation Rangeによる精緻なタイミング調整

Animation Rangeによる精緻なタイミング調整

view-timeline を使うだけでは、アニメーションが開始・終了するタイミングを細かく制御できない場合がある。そこで役立つのが animation-range プロパティだ。これは、要素がビューポートのどの位置に来たときにアニメーションを開始し、どこで終了するかを定義するものだ。

例えば、部品の分解アニメーション(ステージ1)では animation-range: contain cover; が使用された。これは、要素が完全に画面内に入ってから(contain)アニメーションを開始し、画面から消え去るまで(cover)継続することを意味する。一方、回転アニメーション(ステージ2)では、画面から消える前に動きを完結させる必要があるため、animation-range: cover 10% contain; のような指定で調整が行われている。

このように、スクロール量という「時間軸」に対して、アニメーションの「区間」を定義することで、JavaScriptを使わずとも極めて精度の高い演出制御が可能になる。これは、現代のCSSにおける大きな進化の一つだ。

独自の分析:CSS主導のアニメーションがもたらす変化

独自の分析:CSS主導のアニメーションがもたらす変化

今回紹介した手法は、単に「Appleの真似ができる」という以上の意味を持っている。最大の利点は、ブラウザのメインスレッドをJavaScriptの計算から解放できることにある。スクロール駆動アニメーションは、ブラウザのコンポジタースレッドで処理されるため、ページの読み込みや他の処理が重い状況でも、カクつきの少ないスムーズな動きを提供できる。

一方で、実務上の課題も残されている。大量の画像を切り替える手法は、LCP(Largest Contentful Paint)などのパフォーマンス指標に悪影響を与える可能性があるからだ。また、現時点ではFirefoxがこのCSS機能に完全対応していないため、フォールバック(代替表示)の用意が欠かせない。

しかし、これまで「実装コストが高すぎる」と諦めていた高度な演出が、CSS数行で記述できるようになった意義は大きい。今後は、動画ファイルとCSSアニメーションをより高度に組み合わせた、ハイブリッドな実装が主流になっていくのではないかと推測される。

この記事のポイント

  • Apple風のスクロール演出は、部品の分解と回転という2つのステージに分けて考える
  • display: grid を使うことで、要素の重ね合わせとレスポンシブな配置を両立できる
  • view-timelineposition: sticky の組み合わせが、スクロール連動の鍵となる
  • 大量の画像をCSSで切り替える際は、preload による事前読み込みが不可欠だ
  • animation-range を活用することで、JSなしでも精緻な実行タイミングの制御が可能になる
View Transitions APIでサイト体験を劇的に変える!CSSだけで実現する7つのページ遷移レシピ

View Transitions APIでサイト体験を劇的に変える!CSSだけで実現する7つのページ遷移レシピ

Webサイトのページを切り替える際、画面が瞬時にパッと切り替わるのではなく、モバイルアプリのような滑らかなアニメーションを伴う手法が注目されている。これを実現するのが「View Transitions API」だ。複雑なJavaScriptライブラリを使わずに、ブラウザの標準機能とCSSだけで高度な遷移エフェクトを実装できる。

View Transitions APIは主要なブラウザでのサポートが進み、実用的な段階に入った。特にマルチページアプリケーション(MPA)でも、ページ間の連続性を保った演出が可能になった点は大きい。ユーザー体験を向上させるための強力な武器になるだろう。

本記事では、CSS-Tricksの記事を基に、すぐに試せる7つのアニメーションレシピを紹介する。基本的なセットアップから、ぼかしや3D回転を組み合わせた応用例まで、その仕組みを詳しく解説していく。技術的なハードルは低いため、最新のWeb制作トレンドを取り入れたいエンジニアやデザイナーにとって有益な情報となるはずだ。

View Transitions APIの基本設定と導入のポイント

View Transitions APIの基本設定と導入のポイント

View Transitions APIを利用するには、まずブラウザに対して「このサイトでページ遷移のアニメーションを有効にする」という宣言が必要だ。これを「オプトイン(利用選択)」と呼ぶ。CSSの @view-transition アットルールを使い、遷移元と遷移先の両方のページで設定を行う必要がある。

@view-transitionルールでのオプトイン

最も基本的な設定は、CSSに数行のコードを追加するだけで完了する。共通のCSSファイルに記述しておくことで、サイト全体に適用できる。 navigation: auto を指定すると、通常のリンク移動時にブラウザが自動的にトランジションを実行するようになる。

@view-transition {
  navigation: auto;
}

この設定だけで、ブラウザのデフォルトである「クロスフェード(前の画面が消えながら次の画面が浮き上がる)」が適用される。さらに特定の名前(タイプ)を付けることで、ページの種類ごとに異なるアニメーションを使い分けることも可能だ。

ユーザーの好みに配慮したアクセシビリティ対応

アニメーションを実装する上で忘れてはならないのが、アクセシビリティへの配慮だ。OSの設定で「視覚効果を減らす」を選択しているユーザーに対しては、激しい動きを控えるべきだ。これを判定するのが prefers-reduced-motion メディアクエリである。

「動きを減らす」設定が無効(no-preference)の場合のみアニメーションを有効にする記述が推奨される。これにより、すべてのユーザーが快適にサイトを閲覧できる環境を整えられる。技術的な新しさを追求するだけでなく、こうした配慮をセットで行うのがプロの仕事だ。

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: my-transition;
  }
}

視覚効果で魅せるフェードとワイプのレシピ

視覚効果で魅せるフェードとワイプのレシピ

ここからは具体的なアニメーションレシピを見ていこう。まずは定番のフェード効果をアレンジしたものや、画面を拭き取るようなワイプ効果だ。これらは汎用性が高く、どんなジャンルのサイトにも馴染みやすい。

ぼかしを活用したPixelate dissolve

単なるフェードではなく、画面全体をぼかしながら切り替えるのが「Pixelate dissolve(ピクセレート・ディゾルブ)」だ。CSSの filter: blur() プロパティを使用する。古いページがぼやけて消えていき、新しいページがぼやけた状態から鮮明に現れる演出だ。

t=0% (開始・古いページ)
元のコンテンツ(鮮明)
t=50% (ぼかしながらクロスフェード中)
遷移中(ぼやけて溶け合う)
t=100% (完了・新しいページ)
新しいコンテンツ(鮮明)

このアニメーションは実際には約1.4秒かけて連続的に行われる。中間状態の filter:blur(6px)opacity:0.6 がスムーズに変化することで、画面全体が一度ぼやけてから鮮明な新画面が立ち上がる演出になる。短く設定すればキビキビとしたモダンな操作感、長くすればゆったりとした高級感のある印象を与えられる。

clip-pathで実現する上下左右のWipe効果

「Wipe(ワイプ)」は、画面をスライドさせて覆い隠すような効果だ。これには clip-path プロパティの inset() 関数を利用する。 inset() は要素の表示領域を上下左右からの距離で指定する仕組みで、この数値を 0% から 100% へ動かすことで、コンテンツを削り取るような動きを作れる。

例えば「Wipe up(上方向へのワイプ)」なら、古いページの表示領域を下から上へ 100% 削り、新しいページを上から下へ 0% に戻していく。 clip-path を使うメリットは、実際のレイアウトを崩さずに表示領域だけを制御できる点にある。非常にパフォーマンスが良く、滑らかな動きを実現できる。

ダイナミックな動きを作る回転とプッシュの演出

ダイナミックな動きを作る回転とプッシュの演出

次に、より動きの大きいダイナミックな演出を紹介する。これらはユーザーの目を引きやすいため、ポートフォリオサイトやキャンペーンページなど、個性を出したい場面で有効だ。 transform プロパティを駆使して、空間的な広がりを演出する。

遊び心のあるRotate in-out

「Rotate in-out(回転イン・アウト)」は、ページが回転しながら縮小して消え、新しいページが逆回転しながら拡大して現れるエフェクトだ。 scale(0)rotate(180deg) を組み合わせる。実用性は限られるかもしれないが、View Transitionsの表現力の高さを示す良い例だ。

t=0% (開始・元ページ)
元コンテンツ
t=50% (切替の瞬間・縮小して回転中)
回転中
t=100% (完了・新ページ)
新コンテンツ

このアニメーションは実際には約1秒かけて連続的に行われる。中間状態では transform:scale(0.3) rotate(90deg)opacity:0.4 によって元ページが縮小しながら回転して消え、その直後に新ページが逆方向から拡大して現れる。transform-origin: center を指定して画面中央を軸に回転させるのがポイントだ。また、回転角度を大きくしすぎるとユーザーが酔ってしまう可能性があるため、180度程度に抑えておくのが無難だ。

画面の隅から現れるDiagonal push

「Diagonal push(斜めプッシュ)」は、古いページを斜め方向に押し出し、新しいページを逆の斜め方向から滑り込ませる演出だ。 translate(-100%, -100%) のように X軸 と Y軸 の両方を同時に動かすことで斜めの移動を実現する。

この演出は、スライド資料を切り替えるような感覚をユーザーに与える。移動の軌跡に合わせて opacity (不透明度)を変化させると、より自然で洗練された印象になる。 ease (緩急)の指定を工夫することで、重厚感のある動きから軽快な動きまで調整可能だ。

形状と奥行きを活かした高度なトランジション

形状と奥行きを活かした高度なトランジション

最後に、より高度な視覚効果を紹介する。これらは clip-path の応用や 3D変形 を使用しており、実装には少しコツが必要だが、その分インパクトは非常に大きい。ブラウザが自動的に生成するスナップショットをどのように加工するかが鍵となる。

円形に広がるCircle wipe-out

「Circle wipe-out(サークル・ワイプ)」は、画面中央から円形に新しいページが広がっていく演出だ。映画のシーン切り替えなどで見かける手法である。 clip-path: circle() を使い、半径を 0% から 150% まで拡大させることで、画面全体を覆い尽くす動きを作る。

このレシピの面白い点は、背景色が同じページ間での遷移だ。背景が変わらずにコンテンツだけが円形に浮き上がってくるように見えるため、非常にシームレスな体験を提供できる。中心点は at 50% 50% だけでなく、クリックした位置に合わせて動的に変更するような応用も考えられる。

幕が開くようなCurtain reveal

「Curtain reveal(カーテン・リビール)」は、舞台の幕が左右に開くような動きだ。これも clip-path: inset() を使用するが、左右の値を 50% から 0% へと変化させる点が特徴だ。画面中央から左右に向かって新しいページが露出していく様子は、新しい体験の始まりを予感させる。

t=0% (幕が完全に閉じている)
舞台の中身
t=50% (幕が左右に開いて中身が半分見える)
舞台の中身
t=100% (幕が完全に開いて全体が見える)
舞台の中身がフル表示
幕(左右から覆う部分)  新ページのコンテンツ

このアニメーションは実際には約0.8秒かけて連続的に行われる。clip-path: inset(0 50% 0 50%) から inset(0 0 0 0) へと値が変化することで、左右から幕が引かれて中央のコンテンツが露出していく。実際のView Transitionsでは、::view-transition-new(root) に対してこのクリッピングアニメーションを適用することで、滑らかなカーテン効果が実現する。

3D空間でカードがめくれる3D flip

最もインパクトがあるのが「3D flip(3Dフリップ)」だ。ページ全体を一枚のカードに見立て、 Y軸 を中心に回転させて裏返すような演出を行う。 rotateY(90deg) でページを真横に向け、その瞬間に新しいページと入れ替えて 0deg に戻していく。

この演出を成功させるには、 perspective (遠近感)の設定が重要だ。奥行きを感じさせる数値を指定することで、平面的な画面の中に立体的な空間が生まれる。ただし、非常に目立つエフェクトなので、使いどころを慎重に選ぶ必要があるだろう。

実務でView Transitionsを導入する際の注意点

実務でView Transitionsを導入する際の注意点

View Transitions APIは非常に強力だが、実務に導入する際にはいくつか考慮すべき点がある。単にコードをコピーするだけでなく、プロジェクトの要件に合わせた最適化が必要だ。ここでは、技術的な側面とユーザー体験の両面から、筆者の見解を交えて解説する。

ブラウザサポートとフォールバックの考え方

View Transitions APIは現在、ChromeやEdgeなどのChromium系ブラウザで先行して実装され、SafariやFirefoxでも順次対応が進んでいる。しかし、すべてのユーザーが最新ブラウザを使っているわけではない。そのため、「アニメーションが動かなくてもコンテンツは正しく表示される」というプログレッシブ・エンハンスメントの考え方が不可欠だ。

幸いなことに、View Transitions APIは「対応していないブラウザでは単にアニメーションが無視されるだけ」という特性を持っている。特別なJavaScriptによる条件分岐を書かなくても、基本的には安全に導入できる。ただし、アニメーションがあることを前提とした複雑なUI設計は避けるべきだ。

パフォーマンスへの影響と最適化

トランジション実行中、ブラウザは画面のスナップショット(画像のようなもの)を作成し、それをアニメーションさせている。そのため、非常に高解像度な画像が大量にあるページや、複雑なDOM構造を持つページでは、一瞬の動作の重さを感じることがあるかもしれない。

対策としては、 will-change プロパティを適切に使ってブラウザに最適化を促すことや、アニメーションさせる要素を view-transition-name で限定することが有効だ。画面全体(root)を動かすのではなく、ヘッダーやロゴなどの共通要素を固定し、中身のコンテンツだけを動かすようにすると、より軽快で自然な遷移になる。

この記事のポイント

  • View Transitions APIはCSSだけでモバイルアプリのような滑らかなページ遷移を実現する
  • @view-transition ルールの navigation: auto 設定でMPAでも簡単に導入できる
  • clip-pathfilter を組み合わせることで、ぼかしやワイプなど多様な演出が可能になる
  • prefers-reduced-motion を使い、動きを好まないユーザーへの配慮を忘れない
  • 対応ブラウザ以外では通常の遷移になるため、プログレッシブ・エンハンスメントとして導入しやすい