View Transitions provide smooth, animated transitions between pages in your Astro site, creating a single-page application (SPA) experience while maintaining the benefits of multi-page architecture.
Getting Started
Enable view transitions by adding the <ViewTransitions /> component to your layout’s <head>:
---
import { ViewTransitions } from 'astro:transitions' ;
---
< html >
< head >
< meta charset = "utf-8" />
< title > My Site </ title >
< ViewTransitions />
</ head >
< body >
< slot />
</ body >
</ html >
With just this one component, Astro handles all the complexity of page transitions, including routing, animation, and state management.
How It Works
When a user clicks a link:
Intercept Navigation
Astro intercepts the navigation and prevents the default page load
Fetch New Page
The new page is fetched in the background
Animate Transition
Elements fade out, move, or morph based on their transition names
Update Content
The DOM is updated with the new page content
Complete
Elements animate in and the transition completes
Basic Transitions
By default, all pages get a cross-fade transition. Customize transitions using the transition:* directives:
---
import { fade , slide } from 'astro:transitions' ;
---
< html >
< body >
< header transition:persist >
< nav >
< a href = "/" > Home </ a >
< a href = "/about" > About </ a >
</ nav >
</ header >
< main transition:animate = { slide ({ duration: '0.3s' }) } >
< h1 > Welcome </ h1 >
</ main >
</ body >
</ html >
Transition Directives
transition:name
Persist or morph elements across pages by giving them the same name:
src/pages/products/[id].astro
---
const { id } = Astro . params ;
const product = await getProduct ( id );
---
< img
src = { product . image }
alt = { product . name }
transition:name = { `product- ${ id } ` }
/>
< h1 transition:name = { `title- ${ id } ` } >
{ product . name }
</ h1 >
Elements with the same transition:name will smoothly morph between pages:
src/pages/products/index.astro
---
const products = await getProducts ();
---
< div class = "grid" >
{ products . map ( product => (
< a href = { `/products/ ${ product . id } ` } >
< img
src = { product . image }
alt = { product . name }
transition : name = { `product- ${ product . id } ` }
/>
< h2 transition : name = { `title- ${ product . id } ` } >
{ product . name }
</ h2 >
</ a >
)) }
</ div >
When navigating from the grid to a product page, the image and title smoothly animate from their list position to their detail position.
transition:persist
Keep elements in the DOM across page transitions:
< video
src = "/video.mp4"
controls
transition:persist
/>
< audio
src = "/music.mp3"
controls
transition:persist
/>
Persisted elements maintain their state (video position, form inputs, etc.).
transition:animate
Customize animation styles:
---
import { fade , slide } from 'astro:transitions' ;
---
<!-- Fade animation -->
< div transition:animate = { fade ({ duration: '0.5s' }) } >
Content
</ div >
<!-- Slide animation -->
< div transition:animate = { slide ({ duration: '0.3s' }) } >
Slides in from the right
</ div >
<!-- No animation -->
< div transition:animate = "none" >
Instant swap
</ div >
Built-in Animations
---
import { fade } from 'astro:transitions' ;
---
< div transition:animate = { fade ({ duration: '0.4s' }) } >
Fades in and out
</ div >
---
import { slide } from 'astro:transitions' ;
---
< div transition:animate = { slide ({ duration: '0.3s' }) } >
Slides from right to left
</ div >
< div transition:animate = { {
old: {
name: 'slideOut' ,
duration: '0.3s' ,
easing: 'ease-in'
},
new: {
name: 'slideIn' ,
duration: '0.3s' ,
easing: 'ease-out'
}
} } >
Custom animation
</ div >
< style >
@keyframes slideOut {
from { opacity : 1 ; transform : translateX ( 0 ); }
to { opacity : 0 ; transform : translateX ( -100 % ); }
}
@keyframes slideIn {
from { opacity : 0 ; transform : translateX ( 100 % ); }
to { opacity : 1 ; transform : translateX ( 0 ); }
}
</ style >
Directional Animations
Create different animations for forward and backward navigation:
---
import { slide } from 'astro:transitions' ;
---
< main transition:animate = { slide ({ duration: '0.3s' }) } >
< h1 > Content </ h1 >
</ main >
The slide animation automatically:
Slides left when going forward
Slides right when going back
Lifecycle Events
Listen to transition events to run code during transitions:
< script >
document . addEventListener ( 'astro:before-preparation' , ( event ) => {
console . log ( 'About to prepare for transition' );
});
document . addEventListener ( 'astro:after-preparation' , ( event ) => {
console . log ( 'Preparation complete' );
});
document . addEventListener ( 'astro:before-swap' , ( event ) => {
console . log ( 'About to swap content' );
});
document . addEventListener ( 'astro:after-swap' , ( event ) => {
console . log ( 'Content swapped' );
});
document . addEventListener ( 'astro:page-load' , ( event ) => {
console . log ( 'Page load complete' );
});
</ script >
Event Reference
Fires before fetching the new page
Fires after fetching and parsing the new page
Fires before updating the DOM
Fires after updating the DOM
Fires when the page is fully loaded and interactive
Preventing Transitions
Disable transitions for specific links:
<!-- Regular link with transition -->
< a href = "/page" > Transitions enabled </ a >
<!-- Link without transition -->
< a href = "/page" data-astro-reload > No transition </ a >
<!-- External links automatically skip transitions -->
< a href = "https://example.com" > External link </ a >
Fallback Behavior
View Transitions gracefully degrade in browsers that don’t support the View Transitions API:
Modern browsers: Smooth animated transitions
Older browsers: Standard page navigation
JavaScript disabled: Normal links work as expected
The View Transitions API is supported in Chrome, Edge, and other Chromium browsers. Safari and Firefox users get standard navigation.
Preserving State
Persist form state across navigation:
< form transition:persist = "search-form" >
< input
type = "search"
name = "q"
placeholder = "Search..."
/>
< button type = "submit" > Search </ button >
</ form >
Keep videos and audio playing:
< video
src = "/background.mp4"
autoplay
loop
muted
transition:persist
/>
Third-Party Scripts
Persist widgets that shouldn’t reinitialize:
< div id = "chat-widget" transition:persist >
<!-- Chat widget loads once and persists -->
</ div >
Advanced Patterns
Loading Indicators
Show loading state during transitions:
src/components/LoadingBar.astro
< div id = "loading-bar" class = "loading-bar" ></ div >
< style >
.loading-bar {
position : fixed ;
top : 0 ;
left : 0 ;
width : 0 ;
height : 3 px ;
background : linear-gradient ( 90 deg , #667eea 0 % , #764ba2 100 % );
transition : width 0.3 s ease ;
z-index : 9999 ;
}
.loading-bar.active {
width : 100 % ;
}
</ style >
< script >
const bar = document . getElementById ( 'loading-bar' );
document . addEventListener ( 'astro:before-preparation' , () => {
bar ?. classList . add ( 'active' );
});
document . addEventListener ( 'astro:after-swap' , () => {
setTimeout (() => {
bar ?. classList . remove ( 'active' );
}, 300 );
});
</ script >
Customize scroll behavior:
< script >
// Scroll to top on navigation
document . addEventListener ( 'astro:after-swap' , () => {
window . scrollTo ({ top: 0 , behavior: 'smooth' });
});
// Preserve scroll position
let scrollPos = 0 ;
document . addEventListener ( 'astro:before-preparation' , () => {
scrollPos = window . scrollY ;
});
document . addEventListener ( 'astro:after-swap' , () => {
window . scrollTo ({ top: scrollPos });
});
</ script >
Page-Specific Transitions
Different animations for different pages:
---
import { fade } from 'astro:transitions' ;
---
< main transition:animate = { fade ({ duration: '0.5s' }) } >
< h1 > Home </ h1 >
</ main >
---
import { slide } from 'astro:transitions' ;
---
< main transition:animate = { slide ({ duration: '0.3s' }) } >
< h1 > Gallery </ h1 >
</ main >
View Transition Scope
Create isolated transition contexts:
---
import { createAnimationScope } from 'astro:transitions' ;
const scope = createAnimationScope ( 'my-section' );
---
< section data-transition-scope = { scope } >
< img
src = "/image.jpg"
transition:name = "hero"
transition:scope = { scope }
/>
</ section >
Use transition:name sparingly
Only add transition:name to elements that truly need morphing. Too many can impact performance.
Keep animations short
Transitions under 300ms feel snappy. Avoid durations over 500ms.
Optimize images
Use optimized images for smoother transitions, especially for morphing elements.
Test on slow connections
View transitions wait for the new page to load. Test on throttled connections.
Accessibility
View Transitions respect user preferences:
/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
:root {
--animation-duration : 0.01 ms !important ;
}
}
Astro automatically:
Announces page changes to screen readers
Updates the document title
Manages focus appropriately
Respects prefers-reduced-motion
Troubleshooting
Ensure <ViewTransitions /> is in your layout’s <head>
Check that you’re using the same layout across pages
Verify the View Transitions API is supported in your browser
Use transition:persist for elements that should maintain state
Ensure CSS is loaded before the transition starts
Check for conflicting animations
Use the astro:page-load event instead of DOMContentLoaded
Wrap script logic in event listeners that re-run on navigation
Islands Client-side interactivity
Routing Page routing and navigation
Layouts Shared page layouts
Performance Optimization techniques