Skip to content
Docs · 11 pages

Docs / Start here

Runtime

The lifecycle substrate every scripted component sits on, and the API you can use for your own scripts.

2 min readThis page as MarkdownSource on GitHub

Astro bundles a component’s <script> once per page and never re-runs it after a <ClientRouter /> navigation. On pages without the router the astro:* events do not exist at all. On Astro 5.0 to 5.4, astro:page-load could be skipped on a first visit or doubled on back/forward (withastro/astro#12858, fixed in 5.5.0). And it can be lost for a whole page view: the router fires it from the promise that waits for the new page’s scripts, so a navigation interrupted after its swap, or one script request that stalls, leaves the page on screen with nothing bound. Every scripted component in this library goes through one module that owns that problem, and you can use it for your own code. It binds on the swap as well as on the load event, and one setup that throws is reported to the console without stopping the others.

import { onMount, onPage, prefersReducedMotion, refresh } from '@moonarc/core/runtime';

onMount(selector, setup, options?)

Binds setup to every element matching selector, now and again after every navigation. Each element is bound exactly once, however many times events fire.

onMount<HTMLElement>('[data-tilt]', (el, { signal, reducedMotion }) => {
  if (reducedMotion) return;
  el.addEventListener('pointermove', track, { signal }); // removed at teardown
  return () => el.style.removeProperty('--rx'); // runs on astro:before-swap
});
  • signal is an AbortSignal aborted at teardown. Pass it to addEventListener and the listener is removed for you.
  • The returned function runs before the DOM is swapped.
  • options.root scopes the query (default document).
  • Returns an unregister function.

onPage(setup)

One run per page view, with the same lifecycle. For things without an element: a ticker, a GSAP context, a resize observer.

onPage(({ signal }) => {
  const id = setInterval(tick, 1000);
  return () => clearInterval(id);
});

prefersReducedMotion()

Reads the media query once and keeps it current. Prefer shortening motion to near-zero over skipping the code path, so end states still apply.

refresh()

Binds elements you injected yourself after load, and releases bound elements you took out of the page: their listeners go, and their cleanup runs.

Where to register

Every page <script> is its own module. Two pages with the same script are two registrations, and both would bind the same elements. Put onMount calls in the component that owns the elements, or in a layout.

Runtime cost

About 1.9 kB raw, 1.0 kB gzip, once per site: it is one shared chunk no matter how many components use it. Measured in CI; see performance.