Ecopages0.2.0-rc.4

Client-Side Navigation

The @ecopages/browser-router package enables Single Page Application (SPA) navigation behavior in your Ecopages application. It intercepts link clicks, fetches the next page via fetch, and uses morphdom to efficiently diff and update only the parts of the DOM that changed.

This preserves element state (like audio players, web component internals, or form values) and enables native View Transitions.

Installation

Please run the following command to install the package:

npm install @ecopages/browser-router

Setup

To enable client-side routing, initialize the router in your global script (e.g., src/layouts/base-layout.script.ts).

The simplest approach is to use createRouter, which creates and starts the router in one call:

import { createRouter } from '@ecopages/browser-router/client';
 
const router = createRouter({
	viewTransitions: true,
});

For manual control over when the router starts and stops, use the EcoRouter class directly:

import { EcoRouter } from '@ecopages/browser-router/client';
 
const router = new EcoRouter({
	viewTransitions: true,
});
 
router.start();

Configuration

You can customize the router behavior by passing an options object:

OptionTypeDefaultDescription
linkSelectorstring'a[href]'Selector for links to intercept.
documentElementAttributesToSyncstring[]['lang', 'dir', 'data-eco-document-owner']<html> attributes to sync from the incoming document. Attributes not listed here are preserved on the live page.
persistAttributestring'data-eco-persist'Attribute to mark elements that should persist across navigations.
reloadAttributestring'data-eco-reload'Attribute (on link or element) to force a full hard reload.
scrollBehavior'top' | 'preserve' | 'auto''top'Controls scroll behavior after navigation.
smoothScrollbooleanfalseEnables smooth scrolling.
viewTransitionsbooleantrueEnables View Transitions API. Disables UA root crossfade by default. false disables.
prefetchPrefetchConfig | falseObjectConfiguration for prefetching behavior.

View Transitions

The browser router supports the native View Transitions API.

When enabled (default), the router opts the document out of the UA root view-transition group so dark themes do not flash lighter. startViewTransition runs only when the current or incoming page has matching data-view-transition elements for shared-element morphs.

Visual check: dark background → SPA navigate → no lighter flash with defaults.

To create a shared element transition, simply add the data-view-transition attribute to matching elements on different pages:

<!-- Page 1 -->
<div data-view-transition="hero-1">...</div>
 
<!-- Page 2 -->
<div data-view-transition="hero-1">...</div>

The router automatically handles the transition names and ensures a clean "morph" animation (hiding the old snapshot to prevent ghosting) by default.

Directives

You can control the transition behavior using data-view-transition-animate:

ValueDescription
morph(Default) Disables the cross-fade animation, resulting in a clean geometric morph. Ideal for shared element transitions to prevent "ghosting".
fadeUses the standard browser cross-fade animation. Useful if you specifically want the old and new elements to fade into each other.
<!-- Example: Opt-out of the default morph -->
<div data-view-transition="hero-1" data-view-transition-animate="fade">...</div>

Duration

You can also control the speed of a specific transition using data-view-transition-duration:

<div data-view-transition="hero-1" data-view-transition-duration="500ms">...</div>

Update History

| updateHistory | boolean | true | Whether to push a new entry to the browser history for each client-side navigation. | | smoothScroll | boolean | true | Whether to use smooth scrolling when adjusting scroll position after navigation. |

Example with Options

const router = createRouter({
	viewTransitions: true,
	scrollBehavior: 'preserve',
	linkSelector: 'a:not([data-no-route])',
	documentElementAttributesToSync: ['lang', 'dir', 'data-theme'],
});

Use documentElementAttributesToSync to formalize ownership of root <html> attributes. The default keeps navigation metadata in sync while preserving client-managed state such as theme classes or data attributes.

If you need lower-level control without overriding the router pipeline, import the document sync helpers directly:

import {
	createRouter,
	DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC,
	syncDocumentElementAttributes,
} from '@ecopages/browser-router';
 
const router = createRouter();
 
document.addEventListener('eco:before-swap', (event) => {
	syncDocumentElementAttributes(document, event.detail.newDocument, [
		...DEFAULT_DOCUMENT_ELEMENT_ATTRIBUTES_TO_SYNC,
		'data-theme',
	]);
});

Features

Persistence

Elements marked with data-eco-persist are never recreated during navigation. morphdom recognizes these elements by their persist ID and skips updating them entirely, preserving their internal state (event listeners, web component state, form values, audio playback, etc.).

Add the data-eco-persist attribute with a unique ID:

<audio controls src="/music.mp3" data-eco-persist="global-player" />

Script Re-execution

By default, scripts in the <head> are not re-executed during navigation if they already exist. However, some scripts (like hydration logic or analytics) need to run on every page load.

To force a script to re-execute on navigation, add data-eco-rerun="true":

<script type="module" data-eco-rerun="true">
	// This runs on initial load AND every navigation
	initAnalytics(window.location.pathname);
</script>

Optionally, add data-eco-script-id="unique-id" to identify the script. When present, the router removes the previous instance before re-adding the new one, preventing accumulation of duplicate script tags in the head.

<script type="module" data-eco-rerun="true" data-eco-script-id="analytics-tracker">
	// This runs on initial load AND every navigation
	initAnalytics(window.location.pathname);
</script>

How it works:

  • Any script with data-eco-rerun is re-executed on every navigation.
  • If data-eco-script-id is also present, the previous instance is removed first to avoid accumulating duplicate tags.

Prefetching

The router includes a smart prefetching system to speed up navigation. By default, it uses the intent strategy, which prefetches pages when the user hovers over a link or when a link enters the viewport.

Configuration

You can configure prefetching in the createRouter options:

const router = createRouter({
	prefetch: {
		// 'intent' (default) | 'hover' | 'viewport'
		strategy: 'intent',
		// Delay in ms before prefetching on hover (default: 65)
		delay: 65,
		// Attribute to disable prefetching on specific links
		noPrefetchAttribute: 'data-eco-no-prefetch',
		// Whether to respect data-saver mode (default: true)
		respectDataSaver: true,
	},
});

To disable prefetching entirely, set prefetch: false.

Strategies

StrategyDescription
intent(Recommended) Prefetches on hover (with delay) and acts as a fallback for viewport. Balances speed and bandwidth.
hoverPrefetches only when the user hovers over a link.
viewportPrefetches links as soon as they enter the viewport using IntersectionObserver.

Per-Link Control

You can override prefetching behavior on individual links:

<!-- Disable prefetching for this link -->
<a href="/heavy-page" data-eco-no-prefetch>Heavy Page</a>
 
<!-- Force eager prefetching (immediately on load) -->
<a href="/next-page" data-eco-prefetch="eager">Next Page</a>
 
<!-- Use a specific strategy for this link -->
<a href="/about" data-eco-prefetch="hover" data-eco-prefetch-delay="200">About</a>

View Transitions

If viewTransitions is enabled (default true), the router opts the document out of the UA root group and only uses startViewTransition when pages define data-view-transition shared elements. Set false to disable. Optional presets: @import 'ecopages/css/view-transitions.css' and data-eco-transition on elements.

Using Included Styles

The ecopages npm package includes default view transition animations. Import them in your CSS:

@import 'ecopages/css/view-transitions.css';

Then use the data-eco-transition attribute on elements to apply specific animations:

ValueDescription
fadeFades the element in and out during transition.
slideSlides the element horizontally during transition.
zoomScales the element in and out during transition.
slide-upSlides the element vertically upward during transition.
slide-downSlides the element vertically downward during transition.
<main data-eco-transition="slide">
	<!-- Content slides in -->
</main>

Lifecycle Events

The router emits lifecycle custom events on the document object, allowing you to hook into the navigation process.

EventDetailDescription
eco:before-swap{ url, direction, newDocument, reload }Fired after fetching new content but before updating the DOM. Use newDocument to inspect upcoming content. Call reload() to force a hard reload.
eco:after-swap{ url, direction }Fired after the DOM has been updated. Useful for re-initializing scripts or analytics.
eco:page-load{ url, direction }Fired on both initial load and subsequent navigations.

Example: Re-initializing Scripts

document.addEventListener('eco:after-swap', () => {
	console.log('Page updated!');
	// Re-run any page-specific logic here
});

Client-Only Scripts: Scripts listening to client navigation events (eco:after-swap, eco:page-load) should be registered as client scripts (e.g., using scripts: ['./component.script.ts']) or guarded by isServer from @ecopages/radiant/is-server when used in Radiant projects where document is defined during SSR.

Example: Navigation-Aware Components

For components that need to react to URL changes (like updating active states), listen for eco:page-load:

document.addEventListener('eco:page-load', () => {
	// Update active states, re-highlight nav links, etc.
	highlightActiveLink();
});