view-transitions

The View Transitions API provides smooth, native transitions between different views in web applications. Supported in Chrome 126+ and Safari 18.2+.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "view-transitions" with this command: npx skills add yonatangross/orchestkit/yonatangross-orchestkit-view-transitions

View Transitions

The View Transitions API provides smooth, native transitions between different views in web applications. Supported in Chrome 126+ and Safari 18.2+.

Overview

  • Page navigation transitions in SPAs

  • Cross-document (MPA) transitions

  • Shared element animations (image galleries, cards)

  • Modal-to-page transitions

  • List item to detail view animations

  • Tab switching with smooth transitions

Core Patterns

  1. React Router 7.x Integration (Simplest)

import { Link, NavLink, Form } from 'react-router';

// Enable view transitions on links <Link to="/about" viewTransition> About </Link>

// NavLink with viewTransition <NavLink to="/dashboard" viewTransition> Dashboard </NavLink>

// Form with viewTransition <Form method="post" viewTransition> <button type="submit">Save</button> </Form>

  1. useViewTransitionState Hook

import { useViewTransitionState, Link } from 'react-router';

function ProductCard({ product }: { product: Product }) { const isTransitioning = useViewTransitionState(/products/${product.id});

return ( <Link to={/products/${product.id}} viewTransition> <img src={product.image} alt={product.name} style={{ viewTransitionName: isTransitioning ? 'product-image' : undefined, }} /> </Link> ); }

// On detail page, match the transition name function ProductDetail({ product }: { product: Product }) { return ( <img src={product.image} alt={product.name} style={{ viewTransitionName: 'product-image' }} /> ); }

  1. Manual startViewTransition (SPA)

function navigateWithTransition(navigate: NavigateFunction, to: string) { if (!document.startViewTransition) { navigate(to); return; }

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

// With React state updates function handleTabChange(newTab: string) { if (!document.startViewTransition) { setActiveTab(newTab); return; }

document.startViewTransition(() => { ReactDOM.flushSync(() => { setActiveTab(newTab); }); }); }

  1. Cross-Document Transitions (MPA)

/* Enable in both source and target documents */ @view-transition { navigation: auto; }

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

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

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

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

  1. Shared Element Transitions

// Source page (list) function ImageGallery({ images }: { images: Image[] }) { return ( <div className="grid grid-cols-3 gap-4"> {images.map((image) => ( <Link key={image.id} to={/image/${image.id}} viewTransition > <img src={image.thumbnail} alt={image.alt} style={{ viewTransitionName: image-${image.id} }} /> </Link> ))} </div> ); }

// Target page (detail) function ImageDetail({ image }: { image: Image }) { return ( <img src={image.fullSize} alt={image.alt} style={{ viewTransitionName: image-${image.id} }} className="w-full h-auto" /> ); }

  1. Navigation Events (pageswap/pagereveal)

// Customize transition based on navigation type useEffect(() => { const handlePageReveal = (event: PageRevealEvent) => { const transition = event.viewTransition; if (!transition) return;

// Customize based on navigation direction
const fromURL = new URL(navigation.activation?.from || '', location.href);
const toURL = new URL(location.href);

if (isBackNavigation(fromURL, toURL)) {
  transition.types.add('slide-right');
} else {
  transition.types.add('slide-left');
}

};

window.addEventListener('pagereveal', handlePageReveal); return () => window.removeEventListener('pagereveal', handlePageReveal); }, []);

  1. Transition Types for CSS Targeting

// Add transition types programmatically document.startViewTransition({ update: () => navigate(to), types: ['slide-left'], });

/* Target specific transition types */ ::view-transition-group(root) { animation-duration: 0.3s; }

/* Slide left transition */ html:active-view-transition-type(slide-left) { &::view-transition-old(root) { animation: slide-out-left 0.3s ease-out; } &::view-transition-new(root) { animation: slide-in-right 0.3s ease-out; } }

/* Slide right transition (back navigation) */ html:active-view-transition-type(slide-right) { &::view-transition-old(root) { animation: slide-out-right 0.3s ease-out; } &::view-transition-new(root) { animation: slide-in-left 0.3s ease-out; } }

@keyframes slide-out-left { to { transform: translateX(-100%); opacity: 0; } } @keyframes slide-in-right { from { transform: translateX(100%); opacity: 0; } } @keyframes slide-out-right { to { transform: translateX(100%); opacity: 0; } } @keyframes slide-in-left { from { transform: translateX(-100%); opacity: 0; } }

CSS View Transition Pseudo-Elements

/* Structure of view transition pseudo-elements */ ::view-transition ├── ::view-transition-group(root) │ └── ::view-transition-image-pair(root) │ ├── ::view-transition-old(root) │ └── ::view-transition-new(root) └── ::view-transition-group(header) └── ::view-transition-image-pair(header) ├── ::view-transition-old(header) └── ::view-transition-new(header)

/* Customize specific elements */ ::view-transition-group(product-image) { animation-duration: 0.4s; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }

::view-transition-old(product-image), ::view-transition-new(product-image) { /* Prevent default crossfade, use only movement */ animation: none; mix-blend-mode: normal; }

Progressive Enhancement

// Feature detection wrapper function ViewTransitionLink({ to, children, ...props }: LinkProps) { const supportsViewTransitions = typeof document !== 'undefined' && 'startViewTransition' in document;

return ( <Link to={to} viewTransition={supportsViewTransitions} {...props} > {children} </Link> ); }

// CSS feature detection @supports (view-transition-name: none) { .card-image { view-transition-name: var(--transition-name); } }

Accessibility Considerations

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

// Skip transitions for reduced motion function useViewTransition() { const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');

return (callback: () => void) => { if (prefersReducedMotion || !document.startViewTransition) { callback(); return; } document.startViewTransition(callback); }; }

Anti-Patterns (FORBIDDEN)

// ❌ NEVER: Duplicate view-transition-name (must be unique) <img style={{ viewTransitionName: 'image' }} /> <img style={{ viewTransitionName: 'image' }} /> // Breaks transition!

// ❌ NEVER: viewTransitionName on hidden elements <div style={{ display: 'none', viewTransitionName: 'card' }} />

// ❌ NEVER: Missing flushSync with React state updates document.startViewTransition(() => { setState(newValue); // ❌ Won't capture correctly }); // ✅ CORRECT: document.startViewTransition(() => { ReactDOM.flushSync(() => setState(newValue)); });

// ❌ NEVER: Transition during scroll (jank) window.addEventListener('scroll', () => { document.startViewTransition(...); // ❌ Performance issue });

// ❌ NEVER: Long animations blocking interaction ::view-transition-group(root) { animation-duration: 2s; // ❌ Too long, blocks navigation }

// ❌ NEVER: Forgetting progressive enhancement <Link viewTransition>Go</Link> // Breaks in unsupported browsers

Browser Support

Browser Same-Document Cross-Document

Chrome 111+ ✅ ✅ (126+)

Safari 18+ ✅ ✅ (18.2+)

Firefox ❌ (in development) ❌

Edge 111+ ✅ ✅ (126+)

Key Decisions

Decision Option A Option B Recommendation

Transition trigger Auto (MPA) Manual (SPA) Manual for SPAs, auto for MPAs

Animation duration < 200ms 200-400ms 200-300ms balance of UX and speed

Shared elements CSS names JS dynamic CSS for static, JS for dynamic lists

Fallback No animation CSS fallback CSS fallback animations

Reduced motion Instant Shorter animation Instant (skip entirely)

Related Skills

  • motion-animation-patterns

  • Framer Motion for complex animations

  • react-server-components-framework

  • RSC navigation patterns

  • core-web-vitals

  • Performance impact of transitions

  • a11y-testing

  • Testing reduced motion support

Capability Details

same-document-transitions

Keywords: SPA, startViewTransition, React Router, viewTransition Solves: Smooth page transitions in single-page apps

cross-document-transitions

Keywords: MPA, @view-transition, pageswap, pagereveal Solves: Transitions between separate HTML pages

shared-element

Keywords: view-transition-name, morph, hero, image gallery Solves: Shared element animations between pages

navigation-api

Keywords: Navigation API, back/forward, intercept Solves: Customize transitions based on navigation type

fallback-patterns

Keywords: progressive enhancement, feature detection, @supports Solves: Graceful degradation in unsupported browsers

References

  • references/react-router-integration.md

  • React Router 7.x patterns

  • references/mpa-transitions.md

  • Cross-document transitions

  • scripts/view-transition-wrapper.tsx

  • Transition wrapper component

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

responsive-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

domain-driven-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

dashboard-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

rag-retrieval

No summary provided by upstream source.

Repository SourceNeeds Review