Astro Client Navigation
Optimizing navigation speed with prefetching and smooth page transitions.
Core Patterns
- Strategy Selection: Choose
hover(default),tap,viewport, orloadviadata-astro-prefetchattribute based on link priority - Global Config: Enable prefetching site-wide in
astro.config.mjswithprefetch: { defaultStrategy, prefetchAll } - Priority Tiering: Apply
loadto critical next steps (1-2 links max),hoverto navigation,viewportto below-fold links - View Transitions Setup: Add
<ViewTransitions />in<head>to make all navigation SPA-like with smooth animations - Element Persistence: Use
transition:persistto keep elements mounted (audio, video, forms) across page transitions - Morph Transitions: Use
transition:nameon matching elements across pages to create morphing effects - Lifecycle Events: Hook into
astro:before-swap,astro:after-swap, andastro:page-loadto manage state and cleanup - Combined Usage: Use both prefetching and View Transitions together for instant-feeling animated navigation
Prefetching Strategies
Optimizing navigation speed with intelligent prefetching
When to Read This
- Improving perceived performance
- Implementing instant navigation
- Reducing time-to-interactive for links
- Configuring prefetch behavior
Basic Setup
// astro.config.mjs
export default defineConfig({
prefetch: true, // Enable default prefetch behavior
});
or
export default defineConfig({
prefetch: {
defaultStrategy: "hover", // 'hover', 'tap', 'viewport', 'load'
prefetchAll: true,
},
});
Prefetch Strategies
Hover Prefetch (Default)
<!-- Prefetches on hover (300ms delay) -->
<a href="/about">About</a>
<!-- Explicit hover -->
<a href="/contact" data-astro-prefetch="hover">Contact</a>
Use for: Most links — balances performance and data usage.
Tap Prefetch (Mobile-Friendly)
<!-- Prefetches on touchstart/mousedown (before click) -->
<a href="/products" data-astro-prefetch="tap">Products</a>
Use for: High-priority navigation on mobile devices.
Viewport Prefetch (Proactive)
<!-- Prefetches when link enters viewport -->
<a href="/blog" data-astro-prefetch="viewport">Blog</a>
Use for: Content-heavy pages, below-the-fold links.
Load Prefetch (Immediate)
<!-- Prefetches immediately on page load -->
<a href="/dashboard" data-astro-prefetch="load">Dashboard</a>
Use for: Critical next step (signup → dashboard).
Disable Prefetch
<!-- Never prefetch -->
<a href="/external" data-astro-prefetch="false">External</a>
<!-- No prefetch for external links by default -->
<a href="https://example.com">Example</a>
Use for: Large pages, authenticated routes, external links.
Advanced Configuration
Global Configuration
// astro.config.mjs
export default defineConfig({
prefetch: {
defaultStrategy: "hover",
prefetchAll: true, // Prefetch all internal links
},
});
Prefetch with Intent
---
const primaryLinks = ['/pricing', '/features', '/demo'];
const secondaryLinks = ['/about', '/careers', '/blog'];
---
{primaryLinks.map(href => (
<a href={href} data-astro-prefetch="load">{href}</a>
))}
{secondaryLinks.map(href => (
<a href={href} data-astro-prefetch="hover">{href}</a>
))}
Prefetch with View Transitions
---
// src/layouts/Layout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<!-- Prefetch + smooth transition -->
<a href="/about" data-astro-prefetch="hover">About</a>
<slot />
</body>
</html>
Benefit: Instant navigation + smooth animations.
Performance Optimization
Prioritize Critical Links
<nav>
<!-- High-priority (load immediately) -->
<a href="/signup" data-astro-prefetch="load">Sign Up</a>
<a href="/login" data-astro-prefetch="load">Login</a>
<!-- Medium-priority (hover) -->
<a href="/features" data-astro-prefetch="hover">Features</a>
<!-- Low-priority (viewport) -->
<a href="/blog" data-astro-prefetch="viewport">Blog</a>
</nav>
Conditional Prefetch
---
const user = Astro.locals.user;
const isLoggedIn = !!user;
---
<nav>
{isLoggedIn ? (
<a href="/dashboard" data-astro-prefetch="load">Dashboard</a>
) : (
<a href="/login" data-astro-prefetch="hover">Login</a>
)}
</nav>
Save Data Mode
<script>
// Disable prefetch on slow connections
if ('connection' in navigator) {
const connection = navigator.connection;
if (connection.saveData || connection.effectiveType === 'slow-2g') {
document.querySelectorAll('[data-astro-prefetch]').forEach(link => {
link.removeAttribute('data-astro-prefetch');
});
}
}
</script>
Prefetch API Routes
// src/pages/api/products.ts
export async function GET() {
const products = await fetchProducts();
return new Response(JSON.stringify(products), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600",
},
});
}
<script>
// Prefetch data on hover
document.querySelector('#products-link').addEventListener('mouseenter', async () => {
const response = await fetch('/api/products');
const products = await response.json();
sessionStorage.setItem('products', JSON.stringify(products));
});
</script>
<a id="products-link" href="/products">Products</a>
Prefetch Best Practices
- Use
hoveras default — good balance of performance and data usage - Use
loadsparingly — only for critical next steps (1-2 links max) - Disable for large pages — avoid prefetching pages >1MB
- Check
navigator.connection.saveDatafor user preferences - Combine with View Transitions — prefetch + smooth animations = perceived instant
- Use service workers or client-side caching for prefetched pages
Prefetch Edge Cases
Authenticated routes: Middleware can block prefetch requests.
Dynamic content: Prefetched pages may become stale. Use short cache TTLs or disable prefetch.
Mobile data: Use tap or viewport strategies on mobile.
SEO crawlers: Bots don’t trigger prefetch. Ensure pages load without prefetch dependency.
Large pages: Use data-astro-prefetch="false" for pages over 5MB.
Performance Metrics
| Strategy | Trigger | Data Usage | Speed Gain | Mobile-Friendly |
|---|---|---|---|---|
load | Page load | High | Best | Caution |
hover | Hover (300ms) | Medium | Great | N/A |
tap | Mousedown/touch | Low | Good | Yes |
viewport | Enter viewport | Medium | Good | Yes |
false | Never | None | None | Yes |
Prefetch References
View Transitions
Smooth page transitions with native View Transitions API
When to Read This
- Implementing smooth page navigation
- Adding animated transitions between pages
- Customizing transition animations
- Handling transition lifecycle events
- Persisting state across page changes
Basic Setup
---
// src/layouts/Layout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
All navigation becomes SPA-like with smooth transitions.
Transition Directives
Persist Elements Across Pages
---
// Persist header across page transitions
---
<header transition:persist>
<nav>Navigation stays mounted</nav>
</header>
<!-- Audio continues playing across pages -->
<audio transition:persist controls>
<source src="/music.mp3" />
</audio>
Animate Specific Elements
---
import { fade, slide } from 'astro:transitions';
---
<!-- Default fade -->
<div transition:animate="fade">Content</div>
<!-- Slide animation -->
<div transition:animate="slide">Slides in</div>
<!-- Custom animation -->
<div transition:animate={{ name: 'customFade', duration: '0.5s' }}>
Custom timing
</div>
Name Elements for Morph Transitions
<!-- src/pages/index.astro -->
<img src="/hero.jpg" transition:name="hero-image" />
<!-- src/pages/about.astro -->
<!-- Same transition:name creates morphing effect -->
<img src="/hero.jpg" transition:name="hero-image" />
Custom Animations
/* global.css */
@keyframes customSlide {
from {
opacity: 0;
transform: translateY(-100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
::view-transition-old(root) {
animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both customSlide reverse;
}
::view-transition-new(root) {
animation: 300ms cubic-bezier(0.4, 0, 0.2, 1) both customSlide;
}
Per-Element Custom Transitions
<style>
::view-transition-old(hero-image),
::view-transition-new(hero-image) {
animation-duration: 0.5s;
animation-timing-function: ease-in-out;
}
</style>
<img src="/hero.jpg" transition:name="hero-image" />
Lifecycle Events
<script>
document.addEventListener('astro:before-preparation', (event) => {
console.log('Before new page loads');
// Save scroll position, form state, etc.
});
document.addEventListener('astro:after-preparation', (event) => {
console.log('After new page loads, before swap');
});
document.addEventListener('astro:before-swap', (event) => {
console.log('Before DOM swap');
// Clean up event listeners, timers
});
document.addEventListener('astro:after-swap', (event) => {
console.log('After DOM swap, before transition');
// Reinitialize components, restore state
});
document.addEventListener('astro:page-load', (event) => {
console.log('Page fully loaded and transitioned');
// Analytics, scroll restoration
});
</script>
Fallback Behavior
Disable for Specific Links
<!-- External link (no transition) -->
<a href="https://example.com" data-astro-reload>External</a>
<!-- Force full page reload -->
<a href="/page" data-astro-reload>Full Reload</a>
Conditional View Transitions
---
const isMobile = /iPhone|iPad|Android/i.test(Astro.request.headers.get('user-agent'));
---
<html>
<head>
{!isMobile && <ViewTransitions />}
</head>
</html>
Accessibility
CRITICAL: Respect prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.01ms !important;
}
}
View Transitions Best Practices
- Use
transition:persistfor elements that should maintain state (audio, video, forms) - Use
transition:namefor semantic morphing effects - Keep animations short (200-400ms) for perceived performance
- Test on slow devices to ensure transitions don’t cause jank
- Respect user preferences with
prefers-reduced-motion - Provide fallback for browsers without View Transitions API support
View Transitions Edge Cases
SPA mode conflicts: View Transitions work best with MPA routing. Disable View Transitions if using SPA mode.
State persistence: Use transition:persist or save state in localStorage during lifecycle events.
Scroll position: Astro restores scroll by default. Use astro:after-swap to customize.
External libraries: Some libraries may not work with View Transitions. Use data-astro-reload for those pages.
View Transitions References
Combined Patterns
REQUIRED: Combine Prefetch with View Transitions
Use both together for instant-feeling animated navigation.
---
import { ViewTransitions } from 'astro:transitions';
---
<head>
<ViewTransitions />
</head>
<!-- Links automatically prefetched + animated transitions -->
<a href="/about" data-astro-prefetch="hover">About</a>
NEVER: Prefetch Heavy Pages Without Limits
Prefetching with load strategy on many links wastes bandwidth.
// WRONG: prefetchAll with load strategy
export default defineConfig({
prefetch: { defaultStrategy: 'load', prefetchAll: true }
});
// CORRECT: selective prefetch
export default defineConfig({
prefetch: { defaultStrategy: 'hover' }
});
Dark Mode
CSS-Only Dark Mode (prefers-color-scheme)
/* src/styles/global.css */
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #1a1a1a;
--color-text: #ffffff;
}
}
body { background: var(--color-bg); color: var(--color-text); }
Toggle Dark Mode with data-theme
---
// src/layouts/BaseLayout.astro
---
<html data-theme="light">
<head>
<style>
[data-theme="light"] { --color-bg: #fff; --color-text: #111; }
[data-theme="dark"] { --color-bg: #111; --color-text: #fff; }
</style>
<!-- Prevent flash: read stored preference before paint -->
<script is:inline>
const theme = localStorage.getItem('theme')
?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
</script>
</head>
<body><slot /></body>
</html>
---
// src/components/ThemeToggle.astro
---
<button id="theme-toggle" aria-label="Toggle dark mode">
<span class="icon-sun">☀️</span>
<span class="icon-moon">🌙</span>
</button>
<script>
const toggle = document.getElementById('theme-toggle');
toggle?.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
</script>
Dark Mode with Tailwind CSS
// astro.config.mjs — enable class-based dark mode
import tailwind from '@astrojs/tailwind';
export default defineConfig({ integrations: [tailwind()] });
/* src/styles/global.css */
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
<html class={isDark ? 'dark' : ''}>
<body class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<slot />
</body>
</html>
Dark Mode with View Transitions
When using <ViewTransitions />, persist the theme across navigation:
// Persist theme on every page load (including transitions)
document.addEventListener('astro:page-load', () => {
const theme = localStorage.getItem('theme') ?? 'light';
document.documentElement.setAttribute('data-theme', theme);
});