1. Pengenalan View Transitions API
View Transitions API adalah API browser yang memungkinkan Anda membuat animasi transisi antara dua state DOM dengan mudah. Sebelumnya, membuat transisi halaman yang mulus memerlukan library pihak ketiga atau kode JavaScript yang rumit. Dengan View Transitions API, browser melakukan sebagian besar pekerjaan berat.
API ini tersedia di Chrome 111+, Edge 111+, dan Safari 18+. Untuk browser yang belum mendukung, API ini memberikan graceful degradation — konten berubah tanpa animasi, tanpa error.
Apa yang Bisa Dilakukan?
| Fitur | Penjelasan | Contoh |
|---|---|---|
| Same-document | Transisi dalam satu halaman SPA | Ganti tab, toggle view |
| Cross-document | Transisi antar halaman (MPA) | Navigasi page-to-page |
| Shared elements | Elemen yang "berpindah" antara state | Thumbnail → full image |
| Custom animations | CSS @keyframes untuk transisi | Slide, fade, morph |
| Fallback | Tanpa animasi jika tidak didukung | Graceful degradation |
Cara Kerja View Transitions
┌──────────────────────────────────────────────────────────┐ │ VIEW TRANSITIONS FLOW │ ├──────────────────────────────────────────────────────────┤ │ │ │ 1. State Awal (old state) │ │ ┌──────────────┐ │ │ │ Halaman A │ ← Screenshot (capture) │ │ │ [img] [text] │ │ │ └──────────────┘ │ │ │ │ │ 2. DOM Update (callback Anda) │ │ │ │ │ ▼ │ │ 3. State Baru (new state) │ │ ┌──────────────┐ │ │ │ Halaman B │ ← DOM sudah berubah │ │ │ [new content]│ │ │ └──────────────┘ │ │ │ │ │ 4. Browser membuat pseudo-elements: │ │ ::view-transition-old(img) ← screenshot lama │ │ ::view-transition-new(img) ← screenshot baru │ │ │ │ │ 5. Animate kedua screenshot dengan CSS │ │ (fade, slide, morph, dll) │ │ │ │ │ 6. Cleanup: hapus pseudo-elements │ └──────────────────────────────────────────────────────────┘
View Transitions bekerja dengan mengambil screenshot dari state lama dan baru, lalu meng-animasi di antara keduanya. Ini berarti transisi terlihat mulus bahkan untuk perubahan DOM yang kompleks — seperti mengubah layout, menambah/menghapus elemen, atau bahkan berpindah halaman.
2. Same-Document Transitions
Same-document transitions terjadi dalam satu halaman — misalnya saat mengganti tab, toggle dark/light mode, atau memperbarui konten di SPA.
// Cara paling sederhana: document.startViewTransition()
document.startViewTransition(() => {
// Update DOM di dalam callback ini
// Browser akan screenshot state sebelum dan sesudah
document.getElementById('content').innerHTML = `
<h2>Konten Baru</h2>
<p>Ini adalah konten yang sudah diperbarui.</p>
`;
});
// Dengan async callback (untuk fetch data, dll)
document.startViewTransition(async () => {
const response = await fetch('/api/data');
const data = await response.json();
document.getElementById('content').innerHTML = `
<h2>${data.title}</h2>
<p>${data.description}</p>
`;
});
// Tab switching dengan view transition
const tabs = document.querySelectorAll('.tab-btn');
const panels = document.querySelectorAll('.tab-panel');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const targetId = tab.dataset.target;
// Hanya gunakan transition jika browser mendukung
if (document.startViewTransition) {
document.startViewTransition(() => {
switchTab(targetId);
});
} else {
switchTab(targetId);
}
});
});
function switchTab(targetId) {
// Sembunyikan semua panel
panels.forEach(panel => panel.hidden = true);
// Tampilkan panel target
document.getElementById(targetId).hidden = false;
// Update tab active state
tabs.forEach(t => t.classList.remove('active'));
document.querySelector(`[data-target="${targetId}"]`).classList.add('active');
}
// Theme toggle dengan view transition yang cantik
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', () => {
if (!document.startViewTransition) {
document.documentElement.classList.toggle('dark');
return;
}
document.startViewTransition(() => {
document.documentElement.classList.toggle('dark');
});
});
// CSS: animasi khusus untuk theme transition
// ::view-transition-old(root) {
// animation: fade-out 0.3s ease-in;
// }
// ::view-transition-new(root) {
// animation: fade-in 0.3s ease-out;
// }
3. Cross-Document Transitions
Cross-document transitions (juga disebut MPA transitions) memungkinkan animasi saat navigasi antar halaman. Ini bekerja dengan navigasi tradisional (bukan SPA), tanpa perlu JavaScript.
/* Di CSS: aktifkan cross-document view transitions */
/* @view-transition di kedua halaman (asal dan tujuan) */
@view-transition {
navigation: auto; /* Aktifkan untuk semua navigasi */
}
/* Atau hanya untuk navigasi tertentu */
@view-transition {
navigation: auto;
}
/* Atur animasi untuk cross-document */
::view-transition-old(root) {
animation: fade-and-slide-out 0.4s ease-in;
}
::view-transition-new(root) {
animation: fade-and-slide-in 0.4s ease-out;
}
/* Animasi khusus untuk shared elements */
::view-transition-old(hero-image) {
animation: scale-down 0.4s ease-in;
}
::view-transition-new(hero-image) {
animation: scale-up 0.4s ease-out;
}
/* Keyframes */
@keyframes fade-and-slide-out {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(-30px); }
}
@keyframes fade-and-slide-in {
from { opacity: 0; transform: translateX(30px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes scale-down {
from { transform: scale(1); opacity: 1; }
to { transform: scale(0.8); opacity: 0; }
}
@keyframes scale-up {
from { transform: scale(1.2); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
4. Custom Animations
Anda bisa mengkustomisasi animasi transisi menggunakan CSS @keyframes dan pseudo-elements. Setiap elemen yang memiliki view-transition-name mendapatkan pseudo-element sendiri.
/* Default transition (seluruh halaman) */
::view-transition-old(root) {
animation: 0.3s ease-in both fade-out;
}
::view-transition-new(root) {
animation: 0.3s ease-out both fade-in;
}
/* Slide transition */
::view-transition-old(root) {
animation: 0.4s ease-in both slide-out-left;
}
::view-transition-new(root) {
animation: 0.4s ease-out both slide-in-right;
}
/* Zoom/Scale transition untuk shared elements */
::view-transition-old(hero-image) {
animation: 0.4s ease-in both zoom-out;
}
::view-transition-new(hero-image) {
animation: 0.4s ease-out both zoom-in;
}
/* Morph transition: elemen berubah bentuk */
::view-transition-old(card) {
animation: 0.5s ease-in-out both morph-old;
}
::view-transition-new(card) {
animation: 0.5s ease-in-out both morph-new;
}
/* Keyframes */
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-out-left {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-in-right {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes zoom-out {
from { transform: scale(1); opacity: 1; }
to { transform: scale(0.5); opacity: 0; }
}
@keyframes zoom-in {
from { transform: scale(2); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes morph-old {
from { clip-path: inset(0); border-radius: 0; }
to { clip-path: inset(50%); border-radius: 16px; }
}
@keyframes morph-new {
from { clip-path: inset(50%); border-radius: 16px; }
to { clip-path: inset(0); border-radius: 0; }
}
5. view-transition-name
Property view-transition-name menandai elemen sebagai "shared element" — elemen yang tampak "berpindah" antara state lama dan baru. Browser akan secara otomatis meng-animasi perpindahannya.
/* Halaman daftar produk */
.product-list .product-image {
view-transition-name: product-hero;
/* Nama ini harus sama di halaman detail */
}
/* Halaman detail produk */
.product-detail .hero-image {
view-transition-name: product-hero;
/* Browser tahu: ini elemen yang sama! */
/* Akan di-animasi dari posisi lama ke posisi baru */
}
/* Header yang shared antar halaman */
.main-header {
view-transition-name: main-header;
}
/* Card yang expand ke halaman detail */
.card-thumbnail {
view-transition-name: detail-image;
}
.detail-hero {
view-transition-name: detail-image;
}
/* Kustomisasi animasi untuk shared element */
::view-transition-old(product-hero) {
animation: none;
mix-blend-mode: normal;
}
::view-transition-new(product-hero) {
animation: none;
mix-blend-mode: normal;
}
/* Biarkan browser handle morphing secara default */
/* Atau tambahkan animasi custom */
::view-transition-group(product-hero) {
animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
Setiap view-transition-name harus unik dalam satu state DOM. Jika dua elemen di halaman yang sama memiliki nama yang sama, transisi akan gagal. Gunakan nama yang deskriptif dan spesifik.
6. SPA Integration
// ===== Integrasi dengan Router SPA =====
// Contoh: intercept navigasi link
document.addEventListener('click', (e) => {
const link = e.target.closest('a[href]');
if (!link) return;
const href = link.getAttribute('href');
// Hanya untuk navigasi internal
if (!href || href.startsWith('http') || href.startsWith('#')) return;
e.preventDefault();
if (document.startViewTransition) {
document.startViewTransition(async () => {
// Fetch halaman baru
const response = await fetch(href);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Update konten
document.getElementById('main-content').innerHTML =
doc.getElementById('main-content').innerHTML;
// Update title dan URL
document.title = doc.title;
history.pushState({}, '', href);
});
} else {
window.location.href = href;
}
});
// ===== React Router Integration =====
// Di React, gunakan hook:
import { useViewTransition } from './hooks/useViewTransition';
function ProductCard({ product }) {
const navigate = useNavigate();
const startTransition = useViewTransition();
const handleClick = () => {
startTransition(() => {
navigate(`/products/${product.id}`);
});
};
return (
<div onClick={handleClick} style={{ viewTransitionName: `product-${product.id}` }}>
<img src={product.image} />
<h3>{product.name}</h3>
</div>
);
}
// useViewTransition hook
function useViewTransition() {
return (callback) => {
if (document.startViewTransition) {
return document.startViewTransition(callback);
}
callback();
};
}
// Menunggu transisi selesai
const transition = document.startViewTransition(async () => {
await updateDOM();
});
// transition.ready — promise yang resolve saat animasi dimulai
transition.ready.then(() => {
console.log('Animasi transisi dimulai');
});
// transition.finished — promise yang resolve saat animasi selesai
transition.finished.then(() => {
console.log('Animasi transisi selesai');
// Bisa digunakan untuk cleanup atau analytics
});
// Membatalkan transisi
transition.skipTransition();
// DOM tetap berubah, tapi animasi di-skip
7. Fallback Strategies
// Feature detection
function supportsViewTransitions() {
return typeof document.startViewTransition === 'function';
}
// Fallback: animasi CSS sederhana
function transitionWithFallback(updateFn) {
if (supportsViewTransitions()) {
document.startViewTransition(updateFn);
} else {
// Fallback: fade manual
const content = document.getElementById('main-content');
content.classList.add('fade-out');
content.addEventListener('animationend', () => {
updateFn();
content.classList.remove('fade-out');
content.classList.add('fade-in');
}, { once: true });
}
}
// CSS fallback
// .fade-out { animation: 0.3s ease-out fadeOut; }
// .fade-in { animation: 0.3s ease-in fadeIn; }
// @keyframes fadeOut { to { opacity: 0; } }
// @keyframes fadeIn { from { opacity: 0; } }
// Reduced motion: disable transitions
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReducedMotion.matches) {
// CSS: @media (prefers-reduced-motion: reduce) { ... }
}
8. Common Patterns
Image Gallery Expansion
<!-- Gallery view -->
<div class="gallery">
<img src="photo1-thumb.jpg" style="view-transition-name: photo-1">
<img src="photo2-thumb.jpg" style="view-transition-name: photo-2">
<img src="photo3-thumb.jpg" style="view-transition-name: photo-3">
</div>
<!-- Detail view (same element, different layout) -->
<div class="detail-view">
<img src="photo1-full.jpg" style="view-transition-name: photo-1">
<!-- Same view-transition-name = morphing animation! -->
</div>
<style>
.gallery img {
width: 150px;
height: 150px;
object-fit: cover;
border-radius: 8px;
cursor: pointer;
}
.detail-view img {
width: 100%;
max-height: 80vh;
object-fit: contain;
border-radius: 16px;
}
/* Smooth morph between gallery and detail */
::view-transition-group(photo-1),
::view-transition-group(photo-2),
::view-transition-group(photo-3) {
animation-duration: 0.4s;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
/* Keep images sharp during transition */
::view-transition-old(*),
::view-transition-new(*) {
mix-blend-mode: normal;
}
</style>
9. Best Practices
- Selalu gunakan feature detection — cek
document.startViewTransition - Fallback yang baik — pastikan konten tetap berubah tanpa animasi
- Respect reduced-motion — disable animasi untuk pengguna yang prefers-reduced-motion
- Nama yang unik — setiap view-transition-name harus unik per state
- Animasi yang cepat — 200-500ms adalah sweet spot untuk transisi
- Konsistensi — gunakan timing function yang sama untuk elemen terkait
- Jangan terlalu banyak — terlalu banyak shared elements bisa membingungkan
- Test di berbagai browser — Safari 18+ mendukung, tapi perilaku bisa berbeda
- Cross-document butuh @view-transition — perlu deklarasi eksplisit di CSS
- Performance — transisi menggunakan compositor thread, jadi tidak memblokir main thread
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial View Transitions API, jawablah 5 pertanyaan berikut: