Routing & Navigation
The blinc_router crate provides cross-platform routing with path matching, navigation history, guards, page transitions, and deep linking.
Setup
#![allow(unused)] fn main() { use blinc::router::{RouterBuilder, Route, PageTransition}; let router = RouterBuilder::new() .route(Route::new("/").name("home").view(home_page)) .route(Route::new("/users").name("users").view(users_page) .child(Route::new("/:id").name("user").view(user_detail))) .route(Route::new("/settings") .view(settings_page) .transition(PageTransition::modal())) .not_found(not_found_page) .build(); }
Navigation
#![allow(unused)] fn main() { // Push (adds to history) router.push("/users/42"); // Named route with params router.push_named("user", &[("id", "42")]); // Replace (no history entry) router.replace("/login"); // Back / Forward router.back(); router.forward(); // Check state router.can_go_back(); router.current_path(); router.params().get("id"); router.query().get("page"); }
Route Outlet
Place router.outlet() where the page content should render:
#![allow(unused)] fn main() { fn build_ui(ctx: &WindowedContext) -> impl ElementBuilder { div().flex_col() .child(nav_bar(&router)) .child(router.outlet()) // Current route renders here } }
On navigation the outlet swaps its view in place. The scaffold around it (header, nav bar) is left alone, and each navigation builds the view again, even to the same path.
use_router() Hook
Inside route views, use_router() returns the active router:
#![allow(unused)] fn main() { fn user_detail(ctx: RouteContext) -> Div { let router = use_router(); // Same as ctx.router let id = ctx.params.get("id").unwrap_or("?"); div() .child(text(&format!("User #{}", id))) .child( div().on_click(move |_| router.back()) .child(text("Back")) ) } }
Nested routers work automatically — use_router() returns whichever router's outlet() is currently building.
Page Transitions
Per-route transitions using Blinc's animation system:
#![allow(unused)] fn main() { Route::new("/settings") .view(settings_page) .transition(PageTransition::slide()) // iOS push style .transition(PageTransition::fade()) // Crossfade .transition(PageTransition::modal()) // Slide up/down .transition(PageTransition::scale()) // Scale in/out .transition(PageTransition::none()) // Instant // Custom with spring physics .transition(PageTransition::slide().with_spring(SpringConfig::bouncy())) }
A route with a transition plays the new page's enter animation and keeps the old page mounted, out of flow at the start of the outlet, until its exit animation is done.
Navigation Guards
Protect routes with guards that allow, redirect, or reject:
#![allow(unused)] fn main() { use blinc::router::{NavigationGuard, GuardResult}; use std::sync::Arc; let auth_guard: NavigationGuard = Arc::new(|_from, _to| { if is_authenticated() { GuardResult::Allow } else { GuardResult::Redirect("/login".into()) } }); RouterBuilder::new() .route(Route::new("/dashboard").view(dashboard).guard(auth_guard)) .build(); }
Deep Linking
Deep linking is automatic — just build a router and it works on all platforms.
RouterBuilder::build() auto-registers the deep link handler and back button.
#![allow(unused)] fn main() { // That's it — no platform-specific setup needed in Rust let router = RouterBuilder::new() .route(Route::new("/users/:id").view(user_page)) .build(); // Deep links to myapp://host/users/42 automatically navigate }
Platform Configuration
Android — add intent filters in AndroidManifest.xml:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="myapp" />
</intent-filter>
iOS — add URL types in Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>myapp</string></array>
</dict>
</array>
Desktop — register a custom URL scheme with the OS:
macOS (Info.plist):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>myapp</string></array>
</dict>
</array>
Windows (registry, set up by installer):
HKEY_CLASSES_ROOT\myapp\shell\open\command = "C:\path\to\myapp.exe" "--deep-link=%1"
Linux (.desktop file):
MimeType=x-scheme-handler/myapp
Exec=myapp --deep-link=%u
CLI fallback:
myapp --deep-link=myapp://host/users/42
How It Works
RouterBuilder::build()registers a global deep link handler- Platform runners auto-dispatch incoming URIs to the handler
- The router parses the URI and calls
push(path) - No user code needed beyond building the router
System Back Button
Also automatic — RouterBuilder::build() registers a back button handler.
- Android: system back button navigates back if the router has history
- Desktop:
Key::Backdispatches through the back handler stack - If at the root route, the event propagates (app exits normally)
Route Matching
Express-style path patterns:
| Pattern | Example | Matches |
|---|---|---|
| Static | /about | Exact match |
| Parameter | /users/:id | /users/42 → {id: "42"} |
| Wildcard | /files/*path | /files/a/b/c → {path: "a/b/c"} |
| Nested | parent + child | /users + /:id → /users/42 |
| Query | any path | /search?q=hello → {q: "hello"} |
Named Routes
Look up routes by name for type-safe navigation:
#![allow(unused)] fn main() { // Check if a named route exists router.path_for("user"); // Some("/users/:id") // Navigate with params router.push_named("user", &[("id", "42")]); // → /users/42 // Check if a path matches router.has_route("/users/42"); // true }
Tab Navigator
Use the tabs() component from blinc_cn with the router's current path as the active tab:
#![allow(unused)] fn main() { use blinc::cn::tabs; fn app_shell(router: &Router) -> Div { // Track active tab via router path let active_tab = ctx.use_state_keyed("tab", || router.current_path()); div().flex_col().w_full().h_full() // Content area — router outlet .child(router.outlet().flex_grow()) // Bottom tab bar .child( tabs(&active_tab) .tab("Home", "/", { let r = router.clone(); move || r.push("/") }) .tab("Search", "/search", { let r = router.clone(); move || r.push("/search") }) .tab("Profile", "/profile", { let r = router.clone(); move || r.push("/profile") }) ) } }
Each tab click calls router.push() which updates the outlet. The tab state stays in sync with the route.
Stack Navigator (Page Stack)
The router maintains a page stack — pages persist in the tree when new pages are pushed on top. Suspended pages have input disabled and are hidden, but their state (scroll position, form values, etc.) is preserved.
#![allow(unused)] fn main() { // Renders the page stack — active page visible, suspended pages preserved router.outlet() }
When you router.push("/details"):
- The current page becomes Suspended (opacity 0, pointer_events_none)
- The new page is pushed as Active on top
When you router.back():
- The top page is removed from the stack
- The page below becomes Active again (with preserved state)
Page state
#![allow(unused)] fn main() { use blinc::router::PageState; let pages = router.page_stack(); for page in &pages { match page.state { PageState::Active => println!("Visible: {}", page.route.path), PageState::Suspended => println!("Hidden: {}", page.route.path), } } }
Entry/exit animations
Use motion() containers inside route views for animated transitions:
#![allow(unused)] fn main() { fn user_detail(ctx: RouteContext) -> Div { motion() .slide_in(SlideDirection::Right, 300) .child( div().w_full().h_full() .child(text(&format!("User #{}", ctx.params.get("id").unwrap_or("?")))) .child( div().on_click({ let r = ctx.router.clone(); move |_| r.back() }) .child(text("Back")) ) ) } }
Nested Route Stacks
Layout routes can contain their own scoped router for sub-navigation.
use_router() automatically returns the innermost router:
#![allow(unused)] fn main() { fn dashboard_layout(ctx: RouteContext) -> Div { // Create a sub-router for dashboard tabs let sub_router = RouterBuilder::new() .route(Route::new("/").view(dashboard_overview)) .route(Route::new("/analytics").view(analytics)) .route(Route::new("/settings").view(settings)) .initial(&ctx.path) // Start at current sub-path .build(); div().flex_row().w_full().h_full() .child(dashboard_sidebar(&sub_router)) .child(sub_router.outlet()) // Nested outlet — use_router() returns sub_router here } }
Bottom Sheet Navigation
Use sheet() from blinc_cn for modal-like navigation that slides up from the bottom:
#![allow(unused)] fn main() { use blinc::cn::sheet; fn show_details(router: &Router, item_id: &str) { // Navigate to detail route router.push(&format!("/items/{}", item_id)); // Or show as a bottom sheet overlay sheet() .title("Item Details") .content(move || { let router = use_router(); router.outlet() // Render the matched route inside the sheet }) .show(); } }
For gesture-dismissable sheets on mobile, the sheet component handles the swipe-down gesture automatically. On dismiss, call router.back():
#![allow(unused)] fn main() { sheet() .on_close({ let r = router.clone(); move || r.back() }) .content(|| detail_view()) .show(); }
Navigation Patterns Summary
| Pattern | Widget | Router Integration |
|---|---|---|
| Page navigation | router.outlet() | Direct — renders current route |
| Tab bar | blinc::cn::tabs() | Tab clicks call router.push() |
| Stack with animations | stack() + motion() | Wrap route views in motion containers |
| Bottom sheet | blinc::cn::sheet() | Content renders router.outlet(), dismiss calls router.back() |
| Drawer / sidebar | blinc::cn::drawer() | Navigation links call router.push() |
| Back button | Auto-registered | RouterBuilder::build() wires it |
| Deep links | Auto-registered | Platform dispatches to router automatically |