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:
bunx jsr add @ecopages/browser-routerSetup
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:
| Option | Type | Default | Description |
|---|---|---|---|
linkSelector | string | 'a[href]' | Selector for links to intercept. |
persistAttribute | string | 'data-eco-persist' | Attribute to mark elements that should persist across navigations. |
reloadAttribute | string | 'data-eco-reload' | Attribute (on link or element) to force a full hard reload. |
scrollBehavior | 'top' | 'preserve' | 'auto' | 'top' | Controls scroll behavior after navigation. |
smoothScroll | boolean | false | Enables smooth scrolling. |
viewTransitions | boolean | true | Enables View Transitions API support. |
prefetch | PrefetchConfig | false | Object | Configuration for prefetching behavior. |
View Transitions
The browser router supports the native View Transitions API.
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:
| Value | Description |
|---|---|
morph | (Default) Disables the cross-fade animation, resulting in a clean geometric morph. Ideal for shared element transitions to prevent "ghosting". |
fade | Uses 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])',
});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 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"to force execution. - Add
data-eco-script-id="unique-id"to identify the script (preventing duplicate downloads).
<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:
- If the script with
data-eco-script-idalready exists, it is not re-downloaded. - If it has
data-eco-rerun, the router takes care of re-executing it.
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
| Strategy | Description |
|---|---|
intent | (Recommended) Prefetches on hover (with delay) and acts as a fallback for viewport. Balances speed and bandwidth. |
hover | Prefetches only when the user hovers over a link. |
viewport | Prefetches 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: true is enabled, Ecopages will trigger a View Transition on navigation. You can customize the animation using standard CSS or the provided optional styles.
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:
| Value | Description |
|---|---|
fade | Fades the element in and out during transition. |
slide | Slides the element horizontally during transition. |
zoom | Scales the element in and out during transition. |
slide-up | Slides the element vertically upward during transition. |
slide-down | Slides 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.
| Event | Detail | Description |
|---|---|---|
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
});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();
});