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
bunx jsr add @ecopages/browser-routerSetup
To enable client-side routing, initialize the router in your global script (e.g., src/includes/html.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();
// Later, if needed:
// router.stop();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. |
viewTransitions | boolean | false | Enables View Transitions API support. |
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"
/>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 package includes default fade and slide animations. Import them in your CSS:
@import '@ecopages/browser-router/styles.css';Then use the data-eco-transition attribute on elements to apply specific animations:
| Value | Description |
|---|---|
slide | Slides the element horizontally 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();
});