dongquoctien

Astro `transition:persist` for a header that doesn't re-mount

Note · Astro `transition:persist` for a header that doesn't re-mount

  • #astro
  • #view-transitions

Astro’s view transitions are great, but by default every island re-hydrates on navigation. For a header with a theme toggle, an audio player, or any island holding state, that’s a flash and a state reset.

transition:persist keeps the DOM node and its hydrated state across the navigation:

---
import ThemeToggle from './ThemeToggle.astro';
---

<header>
  <nav>...</nav>
  <ThemeToggle transition:persist />
</header>

The browser keeps the same <theme-toggle> element. The script inside doesn’t re-run. The toggle state survives.

The one gotcha worth knowing: scripts inside persisted islands don’t re-run on astro:page-load, so any side-effect that should fire per page (analytics, scroll restore, ad refresh) needs to live outside the persisted boundary. Put it in your top-level layout listening for astro:page-load:

<script>
  document.addEventListener('astro:page-load', () => {
    // analytics.track(window.location.pathname)
  });
</script>

For shared-element animation between a list page and a detail page (think: a project card that morphs into the header of the detail view), pair transition:name on both sides:

<!-- list -->
<a href={`/projects/${p.slug}/`}>
  <h3 transition:name={`title-${p.slug}`}>{p.title}</h3>
</a>

<!-- detail -->
<h1 transition:name={`title-${p.slug}`}>{project.title}</h1>

The browser interpolates between the two elements. It’s the cheapest “this site feels designed” upgrade you can make in Astro.