SuperAPI AI Directory

view-transitions

Frontend & websites

View repository

Updated 10-16 00:29

About this skill

View Transitions API

Native browser API for smooth animated transitions between different views. Works in SPAs (JavaScript-triggered) and MPAs (navigation-based).

Browser Support

  • Same-document (SPA): Chrome 111+, Edge 111+, Safari 18+, Firefox 144+
  • Cross-document (MPA): Chrome 126+, Edge 126+, Safari 18.2+

Quick Start - SPA

// Check for support
if (!document.startViewTransition) {
  updateDOM();
  return;
}

// Trigger transition document.startViewTransition(() => { updateDOM(); });


The API:
1. Captures snapshots of current state
2. Runs your callback to update DOM
3. Captures snapshots of new state
4. Animates between old and new states

## Quick Start - MPA

No JavaScript needed. Opt in with CSS on both pages:

```css
@view-transition {
  navigation: auto;
}

Transitions happen automatically on same-origin navigations.

Basic CSS Customization

/* Customize the default cross-fade */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.5s;
}

/* Fade out old view */
::view-transition-old(root) {
  animation: fade-out 0.3s ease-out both;
}

/* Fade in new view */
::view-transition-new(root) {
  animation: fade-in 0.3s ease-in both;
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes fade-in {
  from { opacity: 0; }
}

Named Transitions

Give specific elements their own transition behavior.

/* Assign a name to an element */
.header {
  view-transition-name: header;
}

.main-image {
  view-transition-name: main-image;
}

/* Names must be unique! */
/* Style transitions for named elements */
::view-transition-old(main-image),
::view-transition-new(main-image) {
  animation-duration: 0.4s;
}

/* Keep header fixed during transition */
::view-transition-group(header) {
  animation: none;
}

Pseudo-Element Tree

When a view transition runs, this pseudo-element tree is created:

::view-transition
└─ ::view-transition-group(name)
   └─ ::view-transition-image-pair(name)
      ├─ ::view-transition-old(name)
      └─ ::view-transition-new(name)
Pseudo Purpose
::view-transition Overlay containing all transitions
::view-transition-group(name) Container for size/position animation
::view-transition-image-pair(name) Contains old and new snapshots
::view-transition-old(name) Screenshot of old state
::view-transition-new(name) Live representation of new state

Transition Types

Categorize transitions for different styling:

document.startViewTransition({
  update: () => updateDOM(),
  types: ['slide-left']  // Add type identifiers
});
/* Style based on transition type */
html:active-view-transition-type(slide-left) {
  &::view-transition-old(root) {
    animation: slide-out-left 0.3s ease;
  }
  &::view-transition-new(root) {
    animation: slide-in-right 0.3s ease;
  }
}

@keyframes slide-out-left {
  to { transform: translateX(-100%); }
}

@keyframes slide-in-right {
  from { transform: translateX(100%); }
}

JavaScript API

ViewTransition Object

const transition = document.startViewTransition(() => {
  // Update DOM here
});

// Promises for timing
transition.ready.then(() => {
  // Pseudo-elements created, animation about to start
});

transition.updateCallbackDone.then(() => {
  // DOM update callback finished
});

transition.finished.then(() => {
  // Animation complete
});

// Skip the animation
transition.skipTransition();

Custom JavaScript Animation

const transition = document.startViewTransition(() => updateDOM());

transition.ready.then(() => {
  // Use Web Animations API on pseudo-elements
  document.documentElement.animate(
    [
      { clipPath: 'circle(0% at 50% 50%)' },
      { clipPath: 'circle(100% at 50% 50%)' }
    ],
    {
      duration: 500,
      easing: 'ease-out',
      pseudoElement: '::view-transition-new(root)'
    }
  );
});

React Integration

Basic Hook

function useViewTransition() {
  const startTransition = (callback) => {
    if (!document.startViewTransition) {
      callback();
      return;
    }
    document.startViewTransition(callback);
  };

  return { startTransition };
}

// Usage
function Component() {
  const { startTransition } = useViewTransition();
  const [page, setPage] = useState('home');

  const navigate = (newPage) => {
    startTransition(() => setPage(newPage));
  };

  return (
    <div>
      <nav>
        <button onClick={() => navigate('home')}>Home</button>
        <button onClick={() => navigate('about')}>About</button>
      </nav>
      {page === 'home' && <HomePage />}
      {page === 'about' && <AboutPage />}
    </div>
  );
}

With React Router

import { useNavigate } from 'react-router-dom';

function NavLink({ to, children }) {
  const navigate = useNavigate();

  const handleClick = (e) => {
    e.preventDefault();

    if (!document.startViewTransition) {
      navigate(to);
      return;
    }

    document.startViewTransition(() => {
      navigate(to);
    });
  };

  return <a href={to} onClick={handleClick}>{children}</a>;
}

React 19+ (Experimental)

import { unstable_ViewTransition as ViewTransition } from 'react';

function App() {
  const [page, setPage] = useState('home');

  return (
    <ViewTransition>
      {page === 'home' ? <Home /> : <About />}
    </ViewTransition>
  );
}

Common Patterns

Hero Image Morph

/* List page */
.thumbnail {
  view-transition-name: hero-image;
}

/* Detail page */
.hero-image {
  view-transition-name: hero-image;
}

/* The image will smoothly morph between pages */
::view-transition-group(hero-image) {
  animation-duration: 0.4s;
}

Shared Header

.header {
  view-transition-name: header;
}

/* Keep header in place */
::view-transition-old(header),
::view-transition-new(header) {
  animation: none;
  mix-blend-mode: normal;
}

Slide Transitions

@keyframes slide-from-right {
  from { transform: translateX(100%); }
}

@keyframes slide-to-left {
  to { transform: translateX(-100%); }
}

/* Forward navigation */
html:active-view-transition-type(forward) {
  &::view-transition-old(root) {
    animation: slide-to-left 0.3s ease forwards;
  }
  &::view-transition-new(root) {
    animation: slide-from-right 0.3s ease forwards;
  }
}

/* Back navigation */
html:active-view-transition-type(back) {
  &::view-transition-old(root) {
    animation: slide-from-right 0.3s ease reverse forwards;
  }
  &::view-transition-new(root) {
    animation: slide-to-left 0.3s ease reverse forwards;
  }
}

Circle Reveal

function circleReveal(x, y) {
  const transition = document.startViewTransition(() => updateContent());

  transition.ready.then(() => {
    const maxRadius = Math.hypot(
      Math.max(x, window.innerWidth - x),
      Math.max(y, window.innerHeight - y)
    );

    document.documentElement.animate(
      {
        clipPath: [
          `circle(0px at ${x}px ${y}px)`,
          `circle(${maxRadius}px at ${x}px ${y}px)`
        ]
      },
      {
        duration: 500,
        easing: 'ease-in-out',
        pseudoElement: '::view-transition-new(root)'
      }
    );
  });
}

// Trigger from click position
button.addEventListener('click', (e) => {
  circleReveal(e.clientX, e.clientY);
});

Grid Item Expansion

/* In grid */
.card {
  view-transition-name: var(--card-id);
}

/* When expanded */
.card-detail {
  view-transition-name: var(--card-id);
}
function Card({ id, onClick }) {
  return (
    <div
      className="card"
      style={{ '--card-id': `card-${id}` }}
      onClick={onClick}
    />
  );
}

MPA Cross-Document Transitions

Opt In

/* Add to both pages */
@view-transition {
  navigation: auto;
}

Customize by Page

<!-- page1.html -->
<html class="page-home">

<!-- page2.html -->
<html class="page-about">
/* Styles apply based on destination */
.page-about::view-transition-old(root) {
  animation: slide-out-left 0.3s;
}

Match Elements Across Pages

Use the new match-element value (Chrome 126+):

/* Automatically generate view-transition-name based on element identity */
.product-card {
  view-transition-name: match-element;
}

Or manually assign matching names:

/* page1.html */
.product-thumbnail[data-id="123"] {
  view-transition-name: product-123;
}

/* page2.html */
.product-hero[data-id="123"] {
  view-transition-name: product-123;
}

Accessibility

Respect Reduced Motion

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Skip for Screen Readers

View transitions are purely visual; screen readers see the DOM update immediately.

Debugging

Chrome DevTools:

  1. Open Elements panel
  2. Check "Show view transition pseudo-elements"
  3. Inspect ::view-transition-* elements

Add slow-mo for development:

::view-transition-old(*),
::view-transition-new(*) {
  animation-duration: 3s !important;
}

Best Practices

  1. Keep transitions short - 200-400ms for most cases
  2. Use view-transition-name sparingly - Too many can hurt performance
  3. Avoid layout shifts - Elements should have stable sizes
  4. Test without transitions - Ensure DOM updates work without the API
  5. Handle edge cases - Elements only on one page need custom enter/exit
  6. Use types for direction - Forward vs back navigation should feel different

Fallback Strategy

async function navigate(callback) {
  // Feature detection
  if (!document.startViewTransition) {
    callback();
    return;
  }

  try {
    await document.startViewTransition(callback).finished;
  } catch (e) {
    // Transition was skipped or errored
    // DOM is still updated, just not animated
  }
}