Astro's client-side navigation can replace page content without performing a full document reload. Code that runs only once at initial page load may then keep references to removed elements, miss new elements or attach duplicate listeners.

Scroll-reveal effects expose this lifecycle problem, but the same rules apply to menus, filters, reading progress and other DOM behaviour.

Choose navigation deliberately

Use browser-native view transitions when the requirement is a visual transition between documents and the supported browser behaviour is sufficient. Use Astro's client router when you need its client-side navigation behaviour.

Read the current Astro view transitions guide before choosing. Navigation changes lifecycle assumptions; it is not simply a CSS effect.

Initialise against the current document

With Astro's client router, astro:page-load is dispatched after navigation and on initial page load. Use it to initialise behaviour that depends on the current page DOM.

A scroll reveal can use one observer for the current set of targets:

let revealObserver;

function initialiseReveals() {
  revealObserver?.disconnect();

  const targets = document.querySelectorAll("[data-reveal]");

  if (!("IntersectionObserver" in window)) {
    targets.forEach((target) => target.classList.add("is-visible"));
    return;
  }

  revealObserver = new IntersectionObserver((entries, observer) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      entry.target.classList.add("is-visible");
      observer.unobserve(entry.target);
    }
  }, { threshold: 0 });

  targets.forEach((target) => {
    target.classList.add("reveal-ready");
    revealObserver.observe(target);
  });
}

document.addEventListener("astro:page-load", initialiseReveals);

Disconnecting the previous observer prevents it from retaining old targets. Unobserving each element after it becomes visible avoids unnecessary future callbacks.

Do not hide content until JavaScript succeeds

A reveal effect should be progressive enhancement. Content must remain readable if JavaScript fails or IntersectionObserver is unavailable.

Avoid shipping markup that is permanently hidden by default and depends on a script to reveal it. Prefer a visible baseline and add the reveal class only when the enhancement is ready. If you choose an initially hidden animation, ensure a robust no-script fallback and avoid layout shifts.

Handle the viewport on first render

Elements already inside the viewport should become visible promptly. Use a sensible threshold and test tall elements, short mobile viewports and content near the fold. Do not assume a fixed percentage threshold works for every element size.

The observer callback may run asynchronously. That is normal; it should not leave essential content inaccessible or invisible while waiting.

Respect reduced motion

Keep the default state usable without animation:

[data-reveal] {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: no-preference) {
  [data-reveal].reveal-ready {
    opacity: 0;
    transform: translateY(0.5rem);
    transition: opacity 180ms ease, transform 180ms ease;
  }

  [data-reveal].reveal-ready.is-visible {
    opacity: 1;
    transform: none;
  }
}

In production, apply the readiness class only after the observer is available and make sure the no-script path remains visible. Test reduced motion rather than assuming the CSS preference is inherited by every animation library.

Clean up page-specific behaviour

For listeners that are added per page, keep a reference and remove them before reinitialising. For observers, call disconnect(). For timers, cancel them. Avoid caching DOM nodes across navigations unless the node is deliberately persistent.

Astro also exposes lifecycle events such as astro:before-swap and astro:after-swap. Use the lifecycle hook that matches the work being done and consult the official event documentation; do not initialise the same behaviour in multiple events.

Test the lifecycle, not just the first load

Verify direct page load, client-side navigation, back and forward navigation, repeated navigation without duplicate handlers, elements already visible at initial render, tall elements and narrow viewports, JavaScript disabled or observer unavailable, and reduced-motion preference.

References: Astro view transitions, MDN Intersection Observer, MDN prefers-reduced-motion.

The durable rule is to initialise against the current DOM, clean up what you own and never make important content depend on an animation running.