Web Development

View Transitions API

Buat transisi halaman yang mulus dan elegan menggunakan View Transitions API — same-document transitions, cross-document transitions, custom animations, fallback strategies, dan integrasi dengan SPA frameworks.

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?

FiturPenjelasanContoh
Same-documentTransisi dalam satu halaman SPAGanti tab, toggle view
Cross-documentTransisi antar halaman (MPA)Navigasi page-to-page
Shared elementsElemen yang "berpindah" antara stateThumbnail → full image
Custom animationsCSS @keyframes untuk transisiSlide, fade, morph
FallbackTanpa animasi jika tidak didukungGraceful degradation

Cara Kerja View Transitions

Diagram: View Transition Flow
┌──────────────────────────────────────────────────────────┐
│              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                       │
└──────────────────────────────────────────────────────────┘
💡 Tips

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.

JavaScript — Basic View Transition
// 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>
  `;
});
JavaScript — Tab Switching
// 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');
}
JavaScript — Dark/Light Mode Toggle
// 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.

CSS — Cross-Document View Transitions
/* 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.

CSS — Animasi Transisi Kustom
/* 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.

CSS — Shared Element Transitions
/* 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);
}
⚠️ Perhatian

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

JavaScript — Integrasi dengan SPA Frameworks
// ===== 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();
  };
}
JavaScript — Waiting for Transition
// 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

JavaScript — Feature Detection & Fallback
// 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

HTML + CSS — Gallery to Detail
<!-- 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

✅ View Transitions 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:

Pertanyaan 1: Fungsi JavaScript apa yang memulai view transition?

a) document.animate()
b) document.startViewTransition()
c) window.transition()
d) element.animate()

Pertanyaan 2: Apa perbedaan same-document dan cross-document transitions?

a) Tidak ada perbedaan
b) Same-document dalam satu halaman, cross-document antar halaman
c) Same-document hanya untuk desktop
d) Cross-document hanya untuk SPA

Pertanyaan 3: CSS property apa yang menandai elemen sebagai shared element?

a) transition-name
b) animation-name
c) view-transition-name
d) element-id

Pertanyaan 4: Apa yang terjadi jika browser tidak mendukung View Transitions API?

a) Error dan halaman crash
b) Konten tetap berubah tanpa animasi
c) Halaman blank
d) Animasi menggunakan fallback library

Pertanyaan 5: Durasi animasi transisi yang ideal adalah?

a) 1-2 detik
b) 200-500ms
c) 50-100ms
d) 5-10 detik
🔍 Zoom
100%
🎨 Tema