Client-side Navigation
Moving between pages without a reload — clientSideNavigation swaps the content region on a sidebar link click and keeps loaded data on screen, and trackNavigation keeps the current-entry mark true after a swap.
By default every link in a Sovrium app is an ordinary page load: the browser fetches a new document, and everything on the page — tables, boards, their loaded rows — starts again from nothing. That is simple and always correct. These two options trade a little of that simplicity for a navigation that feels instant.
clientSideNavigation
clientSideNavigation: true on a sidebar turns a click on an in-app link into a content swap. Sovrium fetches only the destination's <main id="main-content"> element and replaces the current one in place; the document itself is never reloaded.
pages:
- name: deals
path: /deals
components:
- type: sidebar
clientSideNavigation: true
groups:
- label: Sales
items:
- { label: Deals, href: /deals }
- { label: Pipeline, href: /pipeline }Declare it on the sidebar of every page that should take part. A page whose sidebar does not set it loads in full when the reader arrives on it, and its own links are ordinary page loads.
What survives the swap. The document, the client runtime, and the data it has already loaded. A table or board the reader has seen renders its rows at once on return — no loading skeleton — and refreshes them in the background. The page title follows the destination, and so do browser back and forward: each re-swaps the region the address bar points at.
The sidebar comes with the content. It sits inside <main id="main-content">, so the server renders it again for the destination with the right entry carrying aria-current="page". You do not need trackNavigation alongside it.
What is never intercepted. A click with a modifier key, a middle click, a link with target="_blank" or download, a link to another origin, a link to /_admin, /api/ or /assets/, a fragment link within the current page, and any link carrying the data-no-spa attribute. Each behaves exactly as it would without the option. Use data-no-spa on a link that must always load a fresh document.
Pages that always load in full. A destination that declares scripts, sets presence: true, or uses a record-bound layout.sidebar is loaded as a whole document, because each of those lives outside the swapped region. The same happens when the destination answers with an error, redirects (to sign-in, for example), or when the page the reader is leaving has a record-bound layout.sidebar or presence: true of its own. The reader still arrives; it is simply a page load.
Listeners keep working. After every swap, sovrium:navigated is dispatched on document, the same event described under trackNavigation below, so a script listening for it sees client-side navigations as it would any other.
One known difference. The sidebar is rendered again with each destination, so its own scroll position returns to the top on every navigation. In a long sidebar the reader keeps their place in the content, not in the list.
trackNavigation
aria-current="page" is resolved on the server, which is right and sufficient for an app whose every navigation is a page load. An app that swaps its content region in place leaves the sidebar mounted and the server's mark frozen on the page the reader has already left — so the one element that answers "where am I" becomes the one element that is wrong.
trackNavigation: true re-derives the mark on the client after a same-document navigation. It is opt-in because it costs a client island, and an app doing only full page loads gains nothing from it.
The mark moves on two signals. popstate — browser back and forward — needs nothing from your app. The other is sovrium:navigated, the event an in-app swapper announces. Update window.location first, then dispatch a CustomEvent on document:
history.pushState({}, '', '/products/widgets')
document.dispatchEvent(
new CustomEvent('sovrium:navigated', { detail: { path: '/products/widgets' } })
)That order is the contract, not a convention: the sidebar reads window.location itself, so detail.path is informational, and dispatching before the location is updated re-derives the mark onto the page the reader is leaving.
activeMatch is re-evaluated by the same rule on the client as on the server, so a prefix entry keeps its mark across a drill-in. A disclosure whose section becomes current opens with it — but is never re-opened after the reader has deliberately collapsed it.
Behaviour
Sidebar Client Current Marker
- The mark moves to the entry matching an announced same-document navigation
- The mark follows browser back and forward, with no cooperating swapper
- activeMatch is re-evaluated by the same rule on the client as on the server
- Without trackNavigation the server mark stays exactly where it was
- A disclosure opens with its section and is never re-opened after a manual collapse
Client-side Navigation Between App Pages
- A sidebar click swaps the content region for the destination without reloading the document
- The document title becomes the destination page title
- The current-entry mark moves to the destination entry without trackNavigation
- Browser back and forward re-swap the content region in the same document
- Rows already loaded on a table or board are on screen at once on return, then refreshed in the background
- A destination answering an error to the partial request is loaded as a whole document instead
- A destination declaring scripts, presence or a layout sidebar is loaded as a whole document
- Modified clicks, new-tab links, data-no-spa links, and console or API links are never intercepted
- A sidebar that does not set clientSideNavigation loads every destination as a whole document
- A destination that needs the client runtime gets it exactly once after a swap
- An edit still waiting to auto-save is saved when a navigation unmounts the grid
Last updated October 5, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.