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
Note

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:

  1. Intercepts link clicks — same-origin <a> tags navigate via fetch() instead of full page load
  2. Swaps <main> — the content selector is replaced with the new page's content
  3. Updates <title> and <meta> description from the new page
  4. Pushes history — URL changes via pushState, back/forward works
  5. Prefetches on hover — after 65ms of hovering a link, pre-fetches the page
  6. 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" or download attribute
  • Modifier keys: Ctrl+click, Cmd+click, middle-click
  • File links: .pdf, .zip, .png, etc.
  • Hash-only links (#section)
  • mailto: and tel: 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 & LayoutsdefineRoutes() for the nested router wired into createSite()
  • Template FrameworkcreateSite() and the SiteConfig the runtime works alongside
  • Tailwind CSS — cache-busted stylesheet integration with the soft-refresh path