Client Runtime
The @bext-stack/framework/client module provides a self-contained client-side script (2-6KB) that adds SPA-like navigation and live reload to bext template sites. No build step needed — it's injected as an inline <script> tag.
Quick start
import { clientRuntime } from "@bext-stack/framework/client";
function RootLayout({ children }) {
return (
<html>
<body>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
<main>{children}</main>
{clientRuntime()}
</body>
</html>
);
}
With zero configuration, this enables:
- Click a link → page content swaps instantly (no full reload)
- Browser back/forward buttons work
- URL updates via
history.pushState
Live reload is off by default (liveReload: false). Pass liveReload: true to enable it —
the auto-detect mode tries SSE first and falls back to ETag polling if the realtime feature
is not compiled in.
Options
clientRuntime({
navigation: true, // Client-side link interception (default: true)
liveReload: true, // Auto-refresh on server rebuild (default: false)
contentSelector: "main", // Element to swap on navigation (default: "main")
viewTransitions: true, // Use View Transitions API (default: true)
pollInterval: 2000, // Polling interval for live reload (default: 2000ms)
sseEndpoint: "/__bext/events?topics=reload", // SSE endpoint (default)
})
Client-side navigation
When navigation: true (the default), the script:
- Intercepts link clicks — same-origin
<a>tags navigate viafetch()instead of full page load - Swaps
<main>— the content selector is replaced with the new page's content - Updates
<title>and<meta>description from the new page - Pushes history — URL changes via
pushState, back/forward works - Prefetches on hover — after 65ms of hovering a link, pre-fetches the page
- View Transitions — when the browser supports
document.startViewTransition(), navigations animate with a smooth crossfade
What's preserved
<nav>,<header>,<footer>— anything outside<main>stays- Scroll position resets to top on navigation
- External stylesheets persist (no re-download)
What's excluded
These are handled as normal (full page load):
- External links (different origin)
- Links with
target="_blank"ordownloadattribute - Modifier keys: Ctrl+click, Cmd+click, middle-click
- File links:
.pdf,.zip,.png, etc. - Hash-only links (
#section) mailto:andtel:links
View Transitions CSS
Add this to your stylesheet for smooth page transitions:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.2s;
}
main {
view-transition-name: main;
}
Live reload modes
See the Live Reload docs for full details. Summary:
| Mode | Config | How it works |
|---|---|---|
| Auto-detect | liveReload: true |
Tries SSE, falls back to polling |
| SSE only | liveReload: "sse" |
Server-Sent Events (requires realtime feature) |
| Polling only | liveReload: "poll" |
HEAD request checks ETag every 2s |
| Off | liveReload: false |
No auto-refresh |
Script sizes
| Configuration | Size |
|---|---|
| Navigation + live reload (auto) | ~6KB |
| Navigation + SSE only | ~5.4KB |
| Navigation + polling only | ~5.6KB |
| Navigation only | ~3.7KB |
| Everything off | ~150B |
Full example
import { createSite } from "@bext-stack/framework/entry";
import { defineRoutes } from "@bext-stack/framework/router";
import { clientRuntime } from "@bext-stack/framework/client";
import { stylesheet } from "@bext-stack/framework";
import { h } from "@bext-stack/framework";
const router = defineRoutes({
layout: ({ children }) => (
<html lang="en">
<head>
<meta charset="utf-8" />
{stylesheet("/styles.css")}
<style>{`
::view-transition-old(root),
::view-transition-new(root) { animation-duration: 0.15s; }
main { view-transition-name: main; }
`}</style>
</head>
<body class="bg-gray-50 min-h-screen">
<nav class="bg-white shadow-sm border-b px-6 py-3 flex gap-6">
<a href="/" class="font-bold text-gray-900">My Site</a>
<a href="/about" class="text-gray-600 hover:text-gray-900">About</a>
<a href="/blog" class="text-gray-600 hover:text-gray-900">Blog</a>
</nav>
<main class="max-w-4xl mx-auto px-4 py-12">
{children}
</main>
<footer class="border-t px-6 py-4 text-sm text-gray-400 text-center">
Built with bext
</footer>
{clientRuntime({ navigation: true, liveReload: true })}
</body>
</html>
),
routes: {
"/": { page: () => <h1 class="text-4xl font-bold">Welcome</h1> },
"/about": { page: () => <div><h1>About</h1><p>We build things.</p></div> },
"/blog": {
page: () => (
<div>
<h1>Blog</h1>
<a href="/blog/hello">First Post</a>
</div>
),
children: {
"/[slug]": {
page: (ctx) => <article><h1>Post: {ctx.params.slug}</h1></article>,
},
},
},
},
});
createSite({ hostname: "my-site.dev", router });
Clicking between pages animates smoothly, URLs update in the address bar, hover-prefetch makes transitions feel instant, and editing source files auto-refreshes the browser.
Related
- Live Reload — full details on SSE vs polling, rebuild modes, and production safety
- PRISM Framework — the server framework whose pages the client runtime navigates between
- Routing & Layouts —
defineRoutes()for the nested router wired intocreateSite() - Template Framework —
createSite()and theSiteConfigthe runtime works alongside - Tailwind CSS — cache-busted stylesheet integration with the soft-refresh path