Skip to content

Three ways to use Brio

Start at the scale your project needs. The same mounting and cleanup model works for a single widget, an enhanced server page, or a client-side app.

ShapeStart with
WidgetOne small mount root inside an existing page.
Multi-page siteServer navigation, with interactive roots on each page.
Single-page appThe optional router and a dedicated route outlet.

Mount a small root without taking over its host page. Non-overlapping roots can share app state; separate apps have isolated app state unless stores are explicitly shared:

const widget = createApp({ state: () => ({ open: false }) });
const handle = widget.mount('#account-menu');
// When the host removes this widget:
handle.unmount();
CleanupResult
handle.unmount()Releases this root.
app.unmount()Releases all roots; the app stays reusable.
app.dispose()Releases all roots and permanently closes the app.

By default, unmount restores the root’s original attributes and child markup. All three methods accept { restore: false } when the host is about to discard the DOM; see page lifecycle.

Use ordinary server routes and HTML for navigation. Mount Brio only around the interactive areas on each page. Full-page links continue to work without Brio or when no client route matches.

Use the optional router when client-side navigation is useful:

import { createApp } from '@jnreis/briojs';
import { createRouter } from '@jnreis/briojs/router';
const router = createRouter([
{ path: '/', component: 'home-page' },
{ path: '/items/:id', component: 'item-page' },
]);
createApp({ router })
.component('home-page', { template: '<h1>Home</h1>' })
.component('item-page', {
template: '<h1>Item <span b-text="$route.params.id"></span></h1>',
setup({ route }) {
return { get tab() { return route.query.tab ?? 'details'; } };
},
})
.mount('#app');
<div id="app">
<nav>
<a href="/">Home</a>
<a href="/items/42">An item</a>
</nav>
<div b-router-outlet></div>
</div>

createRouter() supports static paths and :parameter segments. It uses the History API for same-origin matches, responds to back/forward, and leaves modified clicks, external links, downloads, and unmatched routes to normal browser navigation.

router.route is a reactive, read-only object describing the current location. Templates read it as $route, and component setup receives it as route:

FieldExample for /items/42?tab=reviews#top
matchedtrue (false when nothing matched or a guard blocked)
component'item-page'
path'/items/42' (app-relative, no query or fragment)
params{ id: '42' }
query{ tab: 'reviews' } (the last value wins for repeated keys)
hash'#top' (always '' in hash mode)
fullPath'/items/42?tab=reviews#top': pass it back to navigate() to return here

Fields are tracked individually, so an effect reading $route.query.tab does not rerun for an unrelated hash change; params and query keep their object identity when their contents are unchanged. router.current remains available as a signal holding the same data as a plain RouteMatch (or null).

Read route state inside effects, getters, or templates rather than copying it once in setup: b-router-outlet keeps the mounted page when only the query or hash changes, so pagination such as ?page=2 updates the existing page instead of remounting it. A different component or path remounts the page.

const signedIn = () => session.user ? true : `/login?redirect=${encodeURIComponent(router.route.fullPath)}`;
const router = createRouter([
{ path: '/', component: 'home-page' },
{ path: '/home', redirect: '/' },
{ path: '/old-items/:id', redirect: to => `/items/${to.params.id}` },
{ path: '/settings', component: 'settings-page', guard: signedIn },
{ path: '*', component: 'not-found-page' },
], { guard: to => to.path !== '/maintenance' });
  • A route may have a component, a redirect (which wins), or neither. redirect is a path or a function of the attempted RouteMatch. A route with neither still matches, updates $route, and intercepts links, but leaves the outlet empty: useful when the page is permanent markup and only some URLs add something, such as { path: '/' } next to { path: '/card/:id', component: 'card-dialog' }.
  • A guard(to) returns true to allow, false to block, or a path to redirect to. The global guard option runs first, then the route’s own guard. Blocked links/navigate() calls leave history unchanged and return false; a blocked initial load or back/forward renders nothing ($route.matched is false). Redirects replace the history entry.
  • Guards are reactive. After navigation, Brio re-runs the current route’s guards whenever state they read changes; signing out on /settings redirects immediately. Guards may run more than once per navigation, so keep them pure.
  • path: '*' matches anything else inside the base, including links, which are then intercepted. Only one * route is allowed. Without it, unmatched URLs use normal browser navigation.
  • More than 10 consecutive redirects throw, to surface redirect loops.

Use createRouter(routes, { mode: 'hash' }) when the server cannot rewrite URLs to the SPA entry (static hosting, file: previews). Routes live in the fragment (/my-app/#/items/42?tab=reviews); href('/items/42') returns '#/items/42'. Fragments that do not start with / (#section) remain ordinary in-page anchors and keep the current page.

In history mode, opening base + 'index.html' directly is treated as the / route and the address bar is replaced with the base.

const router = createRouter([
{ path: '/', component: 'home-page' },
{ path: '/users/:username', component: 'user-page' },
], { base: '/my-app/' });
router.base; // '/my-app/'
router.href('/users/ada?sort=name#details'); // '/my-app/users/ada?sort=name#details'
router.navigate('/users/ada?sort=name#details'); // Updates browser history.
router.current.value.path; // '/users/ada'
router.match('/my-app/users/ada'); // Browser pathname -> route.
router.match('/my-app-other/users/ada'); // null
router.match('/users/ada'); // null: outside this base.

The default base is /. /my-app and /my-app/ both normalize to /my-app/, exposed as router.base. Bases must be absolute pathnames, not origins/URLs, and cannot contain queries, fragments, backslashes, whitespace, repeated slashes, or dot segments. Unicode base paths are URL-encoded.

Application paths and browser URLs are different inputs:

API/sourceInput
Route definitionsApp-relative pathname, e.g. /users/:username; no query or fragment.
href(path)App-relative path with optional query/fragment; returns a deployment-aware pathname/search/hash. Does not require a matching route.
navigate(path, { replace? })Same app-relative input as href; returns false without changing history for unmatched routes.
match(browserUrl)Browser URL/pathname with the base already present; never prepends it. Foreign-origin/outside-base URLs return null.
Initial load, Back/Forward, anchor interceptionActual browser URLs, stripped at a path-segment boundary before matching.

App-relative inputs start with a single /. Origins, protocol-relative URLs, and backslashes are rejected; use normal browser navigation for external links. Paths cannot escape the configured base through ... Do not pass an already base-prefixed browser pathname to navigate or href: it would be interpreted as an application path and prefixed again.

current.value.path is always the matched app-relative pathname, excluding query/fragment. Query strings and fragments remain on the generated/browser URL through navigation and history; parameter matching uses only the pathname. Both /my-app and /my-app/ can match the app’s / route.

Use ordinary anchors with deployment-aware href values. Generate those on the server when links must remain usable without JavaScript:

<a href="/my-app/users/ada?sort=name#details">Ada</a>

Client code can generate the same URL with router.href('/users/ada?sort=name#details'). Only same-origin, matching URLs inside the configured base and a mounted root are intercepted. The base is not reapplied to anchors. Same-page fragment links with unchanged query retain native browser behavior.

Configure your server to serve the SPA entry for direct URLs under /my-app/, including /my-app/users/ada. The router does not configure hosting rewrites, asset paths, or server rendering.