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.
| Shape | Start with |
|---|---|
| Widget | One small mount root inside an existing page. |
| Multi-page site | Server navigation, with interactive roots on each page. |
| Single-page app | The optional router and a dedicated route outlet. |
A widget in an existing app
Section titled “A widget in an existing app”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();| Cleanup | Result |
|---|---|
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.
Multi-page site
Section titled “Multi-page site”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.
Single-page app
Section titled “Single-page app”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.
Route state
Section titled “Route state”router.route is a reactive, read-only object describing the current location.
Templates read it as $route, and component setup receives it as route:
| Field | Example for /items/42?tab=reviews#top |
|---|---|
matched | true (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.
Guards, redirects, and the fallback route
Section titled “Guards, redirects, and the fallback route”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, aredirect(which wins), or neither.redirectis a path or a function of the attemptedRouteMatch. 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)returnstrueto allow,falseto block, or a path to redirect to. The globalguardoption runs first, then the route’s own guard. Blocked links/navigate()calls leave history unchanged and returnfalse; a blocked initial load or back/forward renders nothing ($route.matchedis 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
/settingsredirects 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.
Hash mode
Section titled “Hash mode”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.
Deploying under a base path
Section titled “Deploying under a base path”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'); // nullrouter.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/source | Input |
|---|---|
| Route definitions | App-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 interception | Actual 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.