Soft navigation
Soft navigation swaps the main region in place instead of reloading the whole document. Headers, sidebars, and client modules that already ran stay put.
NavigationRoot
Wrap the swapping region once per document, usually in the root layout.
Everything outside <navigation-root> survives a soft nav.
Navigations are still regular GETs that return HTML, so they cache on a CDN.
root_layout.tsx
import { type LayoutArgs, NavigationRoot } from "dashi"; export function RootLayout({ children }: LayoutArgs) { return ( <html> <body> <header>App</header> <NavigationRoot>{children}</NavigationRoot> </body> </html> ); }
With the host present, same-origin links, GET forms, and same-origin form
redirects soft-navigate. The incoming HTML must include <navigation-root> or
the browser does a full load instead.
Without the host, GET is always a full load. POST and other writes can still apply patches if the client runtime is on the page. See Forms.
While a navigation is in flight, the root is aria-busy, and a newer navigation
aborts the old one.
After the swap:
- History updates for back and forward.
- Scroll position restores, including hash targets.
- Focus moves to
[autofocus]or the host. - The head merges in place: matching stylesheets and scripts stay, new CSS loads before the swap.
langupdates.- The title is announced to assistive tech.
Same path with a new hash only updates location.hash.
Opt out
hardNavigation on the <a>, <form>, or submitter skips intercept and does a
real document load. Modifier keys, middle-click, target, and cross-origin
links stay native.
external.tsx
<a href="/report.pdf" hardNavigation>Download</a>;
Client
Call navigate() from dashi/client to soft-navigate from your own modules.
After a successful commit the document fires dashi:navigated (bubbles,
composed) with { url, push }.
filter.ts
import { navigate } from "dashi/client"; select.addEventListener("change", () => { void navigate(`/items?tag={select.value}`); });
nav_link.ts
document.addEventListener("dashi:navigated", (event) => { const { url, push } = event.detail; // Update header or sidebar from url.pathname });
GET form field details live in Forms. How client modules ship is in Client.