パターン集
実戦で使う具体例。コピペして調整するための雛形として。
1. シンプルなクロスフェード
何もしなくてもデフォルトでこの挙動。期間だけ調整したい時はこれ。
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 350ms;
animation-timing-function: cubic-bezier(.2, .8, .2, 1);
}
2. ダーク / ライト切替(クリック位置から円形展開)
Chrome の有名なデモ。クリック地点を起点に、新しいテーマが clip-path で
円形に広がっていく。
JS — クリック座標を CSS 変数で渡す
document.querySelector("#theme-toggle").addEventListener("click", (ev) => {
const apply = () => document.documentElement.classList.toggle("dark")
if (!document.startViewTransition) return apply()
// クリック位置を CSS 変数に渡す
const x = ev.clientX, y = ev.clientY
document.documentElement.style.setProperty("--vt-x", `${x}px`)
document.documentElement.style.setProperty("--vt-y", `${y}px`)
document.documentElement.style.setProperty(
"--vt-r",
`${Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))}px`
)
document.startViewTransition(apply)
})
CSS — old は触らず、new を clip-path で広げる
::view-transition-old(root) {
animation: none; /* 古い見た目はそのまま固定 */
}
::view-transition-new(root) {
animation: reveal 500ms cubic-bezier(.2, .8, .2, 1);
transform-origin: var(--vt-x) var(--vt-y);
}
@keyframes reveal {
from { clip-path: circle(0 at var(--vt-x) var(--vt-y)); }
to { clip-path: circle(var(--vt-r) at var(--vt-x) var(--vt-y)); }
}
ポイント:
- old を
animation: noneで固定 → 古い見た目が「下に居続ける」 - new に
clip-path: circle(...)の reveal アニメを当てる --vt-rはクリック地点から最も遠い角までの距離(最後まで広がる半径)
3. リスト → 詳細(hero 遷移)
サムネイルがそのまま大きな画像に拡大される、よくあるネイティブ風の遷移。
一覧側
<ul class="cards">
<li>
<a href="#detail-3" data-id="3">
<img src="thumbs/3.jpg" style="view-transition-name: card-3" />
</a>
</li>
</ul>
詳細側(同じ name)
<div id="detail-3" class="detail">
<img src="hero/3.jpg" style="view-transition-name: card-3" />
</div>
JS — 切替時に startViewTransition でラップ
document.querySelectorAll(".cards a").forEach((a) => {
a.addEventListener("click", (e) => {
e.preventDefault()
const id = a.dataset.id
const apply = () => showDetail(id)
if (!document.startViewTransition) return apply()
document.startViewTransition(apply)
})
})
CSS — 形だけ補間して image はクロスフェードしない
::view-transition-group(card-3) {
animation-duration: 420ms;
animation-timing-function: cubic-bezier(.2, .8, .2, 1);
}
/* old/new の同時表示によるブレを抑える */
::view-transition-old(card-3) { animation: none; opacity: 0; }
::view-transition-new(card-3) { animation: none; opacity: 1; }
4. ダイアログ open / close
<dialog> 要素の open/close を transition でアニメ化する。
中身に view-transition-name を付けるのがコツ。
<dialog id="modal" style="view-transition-name: modal-card">
<h2>確認</h2>
<p>実行しますか?</p>
</dialog>
<script>
function openModal() {
if (!document.startViewTransition) return modal.showModal()
document.startViewTransition(() => modal.showModal())
}
function closeModal() {
if (!document.startViewTransition) return modal.close()
document.startViewTransition(() => modal.close())
}
</script>
::view-transition-old(modal-card),
::view-transition-new(modal-card) {
animation-duration: 220ms;
animation-timing-function: cubic-bezier(.16, 1, .3, 1);
}
@keyframes modal-in { from { opacity: 0; transform: scale(.96); } }
@keyframes modal-out { to { opacity: 0; transform: scale(.98); } }
::view-transition-new(modal-card) { animation-name: modal-in; }
::view-transition-old(modal-card) { animation-name: modal-out; }
5. 並べ替え(FLIP の代替)
各アイテムに動的な view-transition-name を当てれば、
並び替え後の位置にスムーズに遷移する。FLIP テクニックを手で書かなくて良くなるのが大きい。
function reorder(nextOrder) {
const apply = () => {
list.replaceChildren(...nextOrder.map((id) => itemNode(id)))
}
if (!document.startViewTransition) return apply()
document.startViewTransition(apply)
}
function itemNode(id) {
const li = document.createElement("li")
li.textContent = id
li.style.viewTransitionName = `item-${id}` // 個別の name
return li
}
::view-transition-group(*) {
animation-duration: 350ms;
animation-timing-function: cubic-bezier(.2, .8, .2, 1);
}
6. ページ送り(左右スライド・MPA)
マルチページサイトで、進む/戻るを左右スライドで表現する。
CSS(全ページ共通)
@view-transition { navigation: auto; }
@keyframes slide-out-left { to { transform: translateX(-30%); opacity: 0; } }
@keyframes slide-in-right { from { transform: translateX( 30%); opacity: 0; } }
@keyframes slide-out-right { to { transform: translateX( 30%); opacity: 0; } }
@keyframes slide-in-left { from { transform: translateX(-30%); opacity: 0; } }
html:active-view-transition-type(forward) {
&::view-transition-old(root) { animation: slide-out-left 320ms ease both; }
&::view-transition-new(root) { animation: slide-in-right 320ms ease both; }
}
html:active-view-transition-type(back) {
&::view-transition-old(root) { animation: slide-out-right 320ms ease both; }
&::view-transition-new(root) { animation: slide-in-left 320ms ease both; }
}
遷移先で types を判定
window.addEventListener("pagereveal", (e) => {
if (!e.viewTransition) return
const a = navigation.activation
const isBack =
a.navigationType === "traverse" &&
(a.from?.index ?? 0) > a.entry.index
e.viewTransition.types.add(isBack ? "back" : "forward")
})
7. ヘッダだけは「居続ける」
ページ間でヘッダを滑らかに居座らせるパターン。両ページのヘッダに同じ name を当てる。
.site-header { view-transition-name: site-header; }
::view-transition-group(site-header) {
/* 形は変わらない場合は短めに */
animation-duration: 200ms;
}
::view-transition-old(site-header),
::view-transition-new(site-header) {
animation: none; /* old/new のクロスフェード自体を無効に */
}
old のクロスフェードを切ると、ヘッダ部分は本物の DOM がそのまま見え続けているかのように 振る舞う(実際は old=透明、new=不透明で連続表示されている)。
8. デフォルト挙動を全部上書きしてフェードを「消す」
ナビは即時切替がいい、でも個別要素にだけ transition を入れたい、というケース。
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
/* 個別 name の transition だけ生かす */