A powerful file‑based router for Rust UI frameworks
Framework‑agnostic by design — one route table, every frontend, powered by quoin.
Featuring loaders, suspense, devtools, and full TanStack Router feature parity.
Navi is a file‑based router for Rust UI applications. It is not tied to any single UI framework: the routing engine, loaders, guards, cache and devtools live in framework‑free crates, and all rendering seams are delegated to quoin — one reactive core, many frontend backends. The same route table, components and devtools panel run unmodified on GPUI, Dioxus, Leptos, Xilem and Floem.
| Framework | Backend | Status | Notes |
|---|---|---|---|
| GPUI | quoin gpui feature |
✅ first‑class | Devtools panel, keybindings, run_app! host |
| Dioxus | quoin dioxus feature |
✅ first‑class | Dioxus desktop (wry/tao window) |
| Leptos | quoin leptos feature |
✅ first‑class | CSR, wasm32-unknown-unknown |
| Xilem | quoin xilem feature |
✅ first‑class | Xilem main, rebuild‑pump refresh |
| Floem | quoin floem feature |
✅ first‑class | Floem main, reactive bridge |
Selecting a framework is a feature flag on your crate — see
Quick Start. There is nothing GPUI‑specific inside the router
itself; GPUI subtrees remain reachable through quoin's rsx! escape hatch
(gpui-rsx-classes is enabled automatically with the gpui feature).
- Framework‑agnostic core: routing, loaders, cache, guards and devtools never touch a UI framework — quoin renders the views.
- File‑based routing: define routes intuitively via the filesystem.
- Nested layouts: hierarchical layouts powered by
<Outlet>. - Loaders: built‑in data fetching with caching, stale‑time, and garbage
collection (backed by
rs-query). - Suspense boundaries: granular loading states using
Awaited. beforeLoadhooks: robust route guards with redirect/notFound support.- Navigation blockers: async/sync blockers to prevent data loss from unsaved changes.
- Scroll restoration: automatic memory and restoration of scroll
positions, keyed by path + normalized query, capacity‑bounded, opt‑out via
scroll_restoration: false, with per‑navigationreset_scroll. - Navigation transactions: batch
Push/Replace/Back/Forward/Go(NavOp) applied atomically with a single re‑render (Navigator::transact). - External links:
http(s):,mailto:andtel:hrefs on<Link>are handed to the OS instead of the router. - Devtools: inspect routes, cache, timeline, and state directly in your app — on every supported framework.
- Validation: seamless integration with
validator,garde,validify, andvalico. - Nested search params (
nested-searchfeature): typed, bracket‑nested query strings (filter[status]=open&tags[0]=rust) viaserde_qs. - Codegen: automatic route tree generation from
src/routes. - Type‑safe hooks: macros like
use_params!,use_search!, anduse_loader_data!. - Deterministic matching: static segments always win over dynamic ones, with a ranked radix trie and structural route‑tree validation.
- Link preloading:
Render/Intent(hover + delay) /Viewportstrategies with per‑href de‑duplication. - Event timeline:
BeforeNavigate → BeforeLoad → Resolvedevents feed the devtools timeline and subscriber callbacks.
| Crate | Purpose |
|---|---|
navi-core |
Shared primitives (window keys, ids, config types). |
navi-router-core |
Pure routing engine: patterns, ranked radix trie matching, route‑tree validation. No UI, no async runtime. |
navi-router |
Router runtime: Navigator, RouterProvider, components (Link, Outlet, Awaited, …), blockers, scroll restoration. |
navi-macros |
define_route!, use_params!, use_search!, use_loader_data!, … |
navi-codegen |
Build‑time route‑tree generation from src/routes (navi.config.json). |
navi-devtools |
Framework‑agnostic devtools model (routes, cache, timeline, state). |
navi-test |
Test utilities for router‑based applications. |
All rendering goes through quoin: route UI
is expressed as quoin_ui::Node trees (or component! shells), and quoin's
macro emitters lower them to the selected framework.
Add the Navi crates plus quoin with your framework's feature:
[dependencies]
navi-router = { path = "navi-router" }
navi-macros = { path = "navi-macros" }
quoin = { path = "quoin/quoin", features = ["dioxus"] } # or gpui / leptos / xilem / floemRoutes render as quoin_ui::Node trees — plain data any backend understands
(src/routes/index.rs):
use navi_macros::define_route;
use navi_router::components::Link;
use quoin_ui::{Node, StyleCmd};
fn index_node() -> Node {
Node::div()
.styles([StyleCmd::FlexCol, StyleCmd::Gap(6.0), StyleCmd::Padding(12.0)])
.child(Node::text("Welcome!"))
.child(Link::new("/users/42").child("Open /users/42").build())
.build()
}
define_route!(IndexRoute, path: "/", is_index: true, node: index_node);Richer screens can use quoin's component! macro instead — one source that
compiles for every backend (see examples/demo-core/src/shell.rs).
A root layout (src/routes/__root.rs) nests children through <Outlet>:
use navi_macros::define_route;
use navi_router::components::{Link, Outlet};
use quoin_ui::{Node, StyleCmd};
fn root_node() -> Node {
Node::div()
.styles([StyleCmd::FlexCol])
.child(Link::new("/").child("Home").build())
.child(Outlet::new().build())
.build()
}
define_route!(RootRoute, path: "/", is_layout: true, node: root_node);Set up build.rs to generate the route tree:
fn main() {
let config = navi_codegen::NaviConfig::from_file("navi.config.json")
.expect("Failed to read config");
navi_codegen::generator::write_route_tree(&config).unwrap();
}Note
Ensure navi.config.json is present in your project root (see
Configuration).
The router installation is identical everywhere; only the host bootstrap differs. The bundled demos show all five:
// GPUI / Xilem / Floem — quoin's `run_app!` host (one line):
use navi_demo_core::NaviDemo;
use quoin::run_app;
run_app!(NaviDemo);// Dioxus desktop
fn app() -> Element { rsx! { NaviDemo {} } }
fn main() { dioxus::LaunchBuilder::desktop().launch(app); }// Leptos CSR (wasm32)
#[wasm_bindgen(start)]
pub fn main() {
leptos::mount::mount_to_body(|| view! { <NaviDemo /> });
}Switching framework = swap the quoin feature + the three‑line host. The route table, loaders, guards and devtools do not change.
Initialize the router inside your host (or reuse examples/demo-core/src/host.rs):
use navi_router::{RouterProvider, Location, RouterOptions, NotFoundMode};
mod route_tree { include!("route_tree.gen.rs"); }
let tree = route_tree::build_route_tree();
let router = RouterProvider::new_with_options(
window_id,
window_handle,
Location::new("/"),
tree,
RouterOptions {
not_found_mode: NotFoundMode::Fuzzy,
..Default::default()
},
cx,
);
route_tree::register_routes(cx);examples/ ships one shared demo (demo-core: routes, loaders,
suspense, guards, search params, 404, devtools) with five thin hosts:
| Example | Framework | Command |
|---|---|---|
demo-gpui |
GPUI | cargo run -p navi-demo-gpui |
demo-dioxus |
Dioxus desktop | cargo run -p navi-demo-dioxus |
demo-floem |
Floem | cargo run -p navi-demo-floem |
demo-xilem |
Xilem | cargo run -p navi-demo-xilem |
demo-leptos |
Leptos CSR | cargo build -p navi-demo-leptos --target wasm32-unknown-unknown |
Every host is a 1–3 line bootstrap around the same NaviDemo shell — open
them side by side to see how little framework-specific code exists.
Control code generation behavior via navi.config.json:
{
"routes_directory": "./src/routes",
"generated_route_tree": "./src/route_tree.gen.rs",
"route_token": "route",
"index_token": "index",
"route_file_ignore_prefix": "-"
}The define_route! macro provides a comprehensive, type‑safe way to
configure routes:
define_route!(
MyRoute,
path: "/users/$id",
params: UserParams,
search: UserSearch,
data: User,
loader: |params, executor| async move { /* fetch data */ },
before_load: |ctx| async move { BeforeLoadResult::Ok },
on_enter: |loc| log::info!("entered"),
on_leave: |loc| log::info!("left"),
stale_time: Duration::from_secs(60),
gc_time: Duration::from_secs(300),
meta: { /* custom metadata */ },
node: my_node, // framework-agnostic quoin_ui::Node UI
);| Macro | Description |
|---|---|
use_params!(MyRoute) |
Extracts typed path parameters. |
use_search!(MyRoute) |
Extracts typed search/query parameters. |
use_loader_data!(MyRoute) |
Accesses data returned by the route's loader. |
use_navigate!() |
Programmatically navigate between routes. |
use_matched_route!(MyRoute) |
Gets context about the currently matched route. |
use_route_context!(MyRoute) |
Accesses the full route context. |
| Component | Description |
|---|---|
Link |
Navigation links with built‑in active styling. |
Outlet |
Renders the matched child route within a layout. |
SuspenseBoundary |
Displays a fallback UI while nested routes load. |
Awaited |
Waits for and renders loader data. |
ScrollRestoration |
Automatically saves and restores scroll positions. |
Tip
Press Cmd+Shift+D (or Ctrl+Shift+D on Windows/Linux) to open the
Devtools panel.
The Devtools interface provides four main tabs:
- Routes: Visual tree view and test navigation.
- Cache: Inspect and invalidate the
rs-querycache. - Timeline: Event log with search, filter, and export capabilities.
- State: Overview of router state, navigation blockers, and metadata.
| Feature | Navi Router | TanStack Router |
|---|---|---|
| File‑based routing | ✅ | ✅ |
| Nested layouts | ✅ | ✅ |
| Loaders & Caching | ✅ | ✅ |
beforeLoad guards |
✅ | ✅ |
| Async navigation blockers | ✅ | ✅ |
| Scroll restoration | ✅ | ✅ |
| Built‑in Devtools | ✅ | ✅ |
| Type‑safe search params | ✅ | ✅ |
| Route context | ✅ | ✅ |
| Deterministic route ranking | ✅ | ✅ |
| Link preloading (render/intent) | ✅ | ✅ |
| Multiple UI frameworks from one route table | ✅ | ❌ |
just test # workspace tests
just clippy # workspace lints (pedantic), zero warnings, per optional feature
just fmt # rustfmt check
just verify # fmt + clippy + tests
just coverage # cargo-llvm-cov summary
cargo bench --bench core --bench adapter # criterion benchmarksThe router is engineered for allocation‑free hot paths — see docs/PERFORMANCE.md for the design and docs/BENCHMARKS.md for full before/after numbers:
- Zero‑copy route matching: stack‑collected parameter bindings, interned
Arc<str>segment names, FxHash static‑child lookup,HashMapmaterialized once on success (matching is 14–38% faster across the board). - Per‑frame paths allocate nothing:
use_params!/use_search!/use_loader_data!read navigation‑time caches;Linkactive checks are branch‑and‑byte comparisons (−94%); loaderQueryKeys are memoized per navigation. - Bounded memory everywhere: ring‑buffered event log, capped preload
registry, O(1) scroll‑eviction,
Arc<RouteTree>shared between the core and the adapter (no deep tree clone at startup). - Lock‑free single‑owner history: no
Mutexin the navigation path (back/forward is −67%).
Quality gates (also enforced in CI):
cargo clippy --workspace --all-targets -- -W clippy::pedantic -D warnings— zero warnings, no#[allow]attributes in the codebase.- The four validation integrations (
validator,garde,validify,valico) are mutually exclusive blanket impls, so they are linted and tested per feature instead of under--all-features. clippy.tomlcalibrates a few shape thresholds for the devtools UI module (large render methods) instead of scattering allow attributes.
The rendering stack is quoin (one
reactive core, many frameworks) and the data stack is
rs-query (query cache with
stale‑time/GC); GPUI is supplied by the matching gpui-pre snapshot when
the gpui feature is selected.
See docs/BRAINSTORM.md for the full improvement brainstorm (performance, DRY/KISS, features) and the remaining follow‑ups (deep‑links re‑introduction, coverage gate).