Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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();
}
#![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
}
}

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()))
}

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

  1. RouterBuilder::build() registers a global deep link handler
  2. Platform runners auto-dispatch incoming URIs to the handler
  3. The router parses the URI and calls push(path)
  4. 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::Back dispatches through the back handler stack
  • If at the root route, the event propagates (app exits normally)

Route Matching

Express-style path patterns:

PatternExampleMatches
Static/aboutExact match
Parameter/users/:id/users/42{id: "42"}
Wildcard/files/*path/files/a/b/c{path: "a/b/c"}
Nestedparent + child/users + /:id/users/42
Queryany 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"):

  1. The current page becomes Suspended (opacity 0, pointer_events_none)
  2. The new page is pushed as Active on top

When you router.back():

  1. The top page is removed from the stack
  2. 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();
}
PatternWidgetRouter Integration
Page navigationrouter.outlet()Direct — renders current route
Tab barblinc_cn::tabs()Tab clicks call router.push()
Stack with animationsstack() + motion()Wrap route views in motion containers
Bottom sheetblinc_cn::sheet()Content renders router.outlet(), dismiss calls router.back()
Drawer / sidebarblinc_cn::drawer()Navigation links call router.push()
Back buttonAuto-registeredRouterBuilder::build() wires it
Deep linksAuto-registeredPlatform dispatches to router automatically