References

Astro Client Navigation

Optimizing navigation speed with prefetching and smooth page transitions.

Core Patterns

  • Strategy Selection: Choose hover (default), tap, viewport, or load via data-astro-prefetch attribute based on link priority
  • Global Config: Enable prefetching site-wide in astro.config.mjs with prefetch: { defaultStrategy, prefetchAll }
  • Priority Tiering: Apply load to critical next steps (1-2 links max), hover to navigation, viewport to below-fold links
  • View Transitions Setup: Add <ViewTransitions /> in <head> to make all navigation SPA-like with smooth animations
  • Element Persistence: Use transition:persist to keep elements mounted (audio, video, forms) across page transitions
  • Morph Transitions: Use transition:name on matching elements across pages to create morphing effects
  • Lifecycle Events: Hook into astro:before-swap, astro:after-swap, and astro:page-load to 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

<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

  1. Use hover as default — good balance of performance and data usage
  2. Use load sparingly — only for critical next steps (1-2 links max)
  3. Disable for large pages — avoid prefetching pages >1MB
  4. Check navigator.connection.saveData for user preferences
  5. Combine with View Transitions — prefetch + smooth animations = perceived instant
  6. 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

StrategyTriggerData UsageSpeed GainMobile-Friendly
loadPage loadHighBestCaution
hoverHover (300ms)MediumGreatN/A
tapMousedown/touchLowGoodYes
viewportEnter viewportMediumGoodYes
falseNeverNoneNoneYes

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

<!-- 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

  1. Use transition:persist for elements that should maintain state (audio, video, forms)
  2. Use transition:name for semantic morphing effects
  3. Keep animations short (200-400ms) for perceived performance
  4. Test on slow devices to ensure transitions don’t cause jank
  5. Respect user preferences with prefers-reduced-motion
  6. 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);
});