Part 12 of 14 26 Jan 2026 By Raj Patil 30 min read

Part 12: Routing - Navigation Between Views

Part 12 of the Freya Rust GUI series. Learn to implement routing with type-safe navigation, layouts, dynamic parameters, nested routes, and programmatic navigation.

Intermediate #rust #freya #gui #routing #navigation #spa #tutorial
Building Native GUIs with Rust & Freya 12 / 14

Routing

Most applications have multiple views - home, settings, profile, etc. Routing lets users navigate between them while keeping your code organized.

[!NOTE] What is Routing? Routing maps URLs (or paths) to components. When the URL changes, the router displays the corresponding component. This enables deep linking, browser history, and organized code structure.


Basic Setup

Define Routes

Use the #[derive(Routable)] macro to define your routes:

use freya::prelude::*;
use freya_router::prelude::*;

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
    #[route("/")]
    Home,

    #[route("/settings")]
    Settings,

    #[route("/profile")]
    Profile,
}

[!TIP] Why #[rustfmt::skip]? The macro generates code that rustfmt might reformat in unexpected ways. Skipping formatting keeps the route definitions clean.

Create Router

Set up the router at the root of your application:

fn app() -> impl IntoElement {
    Router::<Route>::new(|| RouterConfig::default().with_initial_path(Route::Home))
}

Route Components

Each route variant renders a component:

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
    #[route("/")]
    #[component(HomePage)]
    Home,

    #[route("/settings")]
    #[component(SettingsPage)]
    Settings,

    #[route("/profile")]
    #[component(ProfilePage)]
    Profile,
}

#[derive(PartialEq)]
struct HomePage;

impl Component for HomePage {
    fn render(&self) -> impl IntoElement {
        label().text("Home Page")
    }
}

Use the Link component for navigation:

Link::new(Route::Settings)
    .child("Go to Settings")
fn nav_link(route: Route, text: &str) -> impl IntoElement {
    let current = use_route::<Route>();

    let is_active = current == route;

    rect()
        .padding_horizontal(16.0)
        .padding_vertical(8.0)
        .background(if is_active {
            Color::from_rgb(79, 70, 229)
        } else {
            Color::TRANSPARENT
        })
        .corner_radius(8.0)
        .child(
            Link::new(route)
                .child(
                    label()
                        .text(text)
                        .color(if is_active {
                            Color::WHITE
                        } else {
                            Color::GRAY
                        })
                )
        )
}

Programmatic Navigation

Navigate from code:

fn navigate_button() -> impl IntoElement {
    Button::new()
        .on_press(|_| {
            RouterContext::get().push(Route::Settings);
        })
        .child("Go to Settings")
}
let router = RouterContext::get();

// Go to a route
router.push(Route::Settings);

// Go back in history
router.go_back();

// Go forward in history
router.go_forward();

// Replace current route (no history entry)
router.replace(Route::Home);

// Get current route
let current: Route = router.current();

Layouts

Use layouts to wrap multiple routes with common UI (navigation, header, footer).

Basic Layout

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
    #[layout(MainLayout)]
        #[route("/")]
        Home,

        #[route("/settings")]
        Settings,

        #[route("/profile")]
        Profile,
}

#[derive(PartialEq)]
struct MainLayout;

impl Component for MainLayout {
    fn render(&self) -> impl IntoElement {
        rect()
            .expanded()
            .direction(Direction::Horizontal)
            .child(Sidebar {})
            .child(Outlet::<Route>::new())  // Child route renders here
    }
}

The Outlet component renders the child route’s content.

Complete Layout Example

fn sidebar() -> impl IntoElement {
    rect()
        .width(Size::px(250.0))
        .height(Size::fill())
        .background(Color::from_rgb(30, 30, 30))
        .padding(16.0)
        .direction(Direction::Vertical)
        .gap(8.0)
        .child(nav_link(Route::Home, "Home"))
        .child(nav_link(Route::Settings, "Settings"))
        .child(nav_link(Route::Profile, "Profile"))
}

impl Component for MainLayout {
    fn render(&self) -> impl IntoElement {
        rect()
            .expanded()
            .direction(Direction::Horizontal)
            .child(sidebar())
            .child(
                rect()
                    .width(Size::fill())
                    .height(Size::fill())
                    .padding(24.0)
                    .child(Outlet::<Route>::new())
            )
    }
}

Route Parameters

Dynamic Segments

Capture URL parameters:

#[derive(Routable, Clone, PartialEq)]
pub enum Route {
    #[route("/users/:id")]
    User { id: u32 },
}

#[derive(PartialEq)]
struct UserPage {
    id: u32,
}

impl Component for UserPage {
    fn render(&self) -> impl IntoElement {
        rect()
            .direction(Direction::Vertical)
            .gap(8.0)
            .child(
                label()
                    .text(format!("User ID: {}", self.id))
                    .font_size(24.0)
            )
            .child(
                label()
                    .text(format!("Profile page for user {}", self.id))
            )
    }
}

Multiple Parameters

#[derive(Routable, Clone, PartialEq)]
pub enum Route {
    #[route("/users/:user_id/posts/:post_id")]
    Post { user_id: u32, post_id: u32 },
}

String Parameters

#[derive(Routable, Clone, PartialEq)]
pub enum Route {
    #[route("/search/:query")]
    Search { query: String },
}

use_route Hook

Access the current route in any component:

fn navigation() -> impl IntoElement {
    let route = use_route::<Route>();

    match route {
        Route::Home => label().text("On Home"),
        Route::Settings => label().text("On Settings"),
        Route::Profile => label().text("On Profile"),
        Route::User { id } => label().text(format!("On User {}", id)),
    }
}

Nested Routes

Organize routes hierarchically:

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
    #[layout(AppLayout)]
        #[route("/")]
        Home,

        #[layout(SettingsLayout)]
            #[route("/settings")]
            Settings,

            #[route("/settings/profile")]
            Profile,

            #[route("/settings/account")]
            Account,

            #[route("/settings/notifications")]
            Notifications,
}

#[derive(PartialEq)]
struct SettingsLayout;

impl Component for SettingsLayout {
    fn render(&self) -> impl IntoElement {
        rect()
            .direction(Direction::Horizontal)
            .gap(24.0)
            .child(
                rect()
                    .width(Size::px(200.0))
                    .direction(Direction::Vertical)
                    .gap(8.0)
                    .child(nav_link(Route::Profile, "Profile"))
                    .child(nav_link(Route::Account, "Account"))
                    .child(nav_link(Route::Notifications, "Notifications"))
            )
            .child(Outlet::<Route>::new())
    }
}

404 Handling

Handle unknown routes gracefully:

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
    #[route("/")]
    Home,

    #[route("/settings")]
    Settings,

    // Catch-all for unknown routes
    #[route("/:..segments")]
    NotFound { segments: Vec<String> },
}

#[derive(PartialEq)]
struct NotFound {
    segments: Vec<String>,
}

impl Component for NotFound {
    fn render(&self) -> impl IntoElement {
        rect()
            .expanded()
            .main_align_center()
            .cross_align_center()
            .direction(Direction::Vertical)
            .gap(16.0)
            .child(
                label()
                    .text("404")
                    .font_size(64.0)
                    .font_weight(FontWeight::Bold)
                    .color(Color::GRAY)
            )
            .child(
                label()
                    .text(format!("Page not found: /{}", self.segments.join("/")))
            )
            .child(
                Link::new(Route::Home)
                    .child(Button::new().child("Go Home"))
            )
    }
}

Route Guards

Protect routes based on conditions (authentication, permissions):

#[derive(PartialEq)]
struct ProtectedRoute;

impl Component for ProtectedRoute {
    fn render(&self) -> impl IntoElement {
        let is_authenticated = use_authenticated();

        if !is_authenticated {
            return rect()
                .expanded()
                .main_align_center()
                .cross_align_center()
                .direction(Direction::Vertical)
                .gap(16.0)
                .child(label().text("Please log in"))
                .child(Link::new(Route::Login).child(Button::new().child("Login")));
        }

        Outlet::<Route>::new()
    }
}

Using Guards in Routes

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
    #[route("/")]
    Home,

    #[route("/login")]
    Login,

    #[layout(ProtectedRoute)]
        #[route("/dashboard")]
        Dashboard,

        #[route("/settings")]
        Settings,
}

Redirects

Redirect from one route to another:

fn redirect_component() -> impl IntoElement {
    let router = RouterContext::get();

    use_effect(move || {
        router.replace(Route::Home);
    });

    rect()  // This won't be seen
}

Conditional Redirect

fn old_route() -> impl IntoElement {
    let router = RouterContext::get();
    let should_redirect = true;

    if should_redirect {
        router.replace(Route::NewRoute);
    }

    rect().child(label().text("Old content"))
}

Router Configuration

Initial Route

Router::<Route>::new(|| {
    RouterConfig::default()
        .with_initial_path(Route::Settings)
})

Memory History

Use memory-based history (no browser URL) for testing or desktop apps:

Router::<Route>::new(|| {
    RouterConfig::default()
        .history(MemoryHistory::new())
})

Link to external URLs:

Link::new("https://example.com")
    .child("External Link")

Complete App Example

#[derive(Routable, Clone, PartialEq)]
#[rustfmt::skip]
pub enum Route {
    #[layout(MainLayout)]
        #[route("/")]
        Home,

        #[route("/users/:id")]
        User { id: u32 },

        #[layout(SettingsLayout)]
            #[route("/settings")]
            Settings,

            #[route("/settings/profile")]
            Profile,

        #[route("/:..segments")]
        NotFound { segments: Vec<String> },
}

fn main() {
    launch(
        LaunchConfig::new()
            .with_window(
                WindowConfig::new_app(|| Router::<Route>::new(|| RouterConfig::default()))
                    .with_title("My App")
                    .with_size(1200.0, 800.0)
            )
    );
}

Best Practices

1. Use Layouts for Shared UI

Don’t repeat navigation in every page - use layouts.

2. Handle 404s Gracefully

Always include a catch-all route.

3. Use Type-Safe Navigation

The Routable derive ensures compile-time checking:

// Compiler catches typos
router.push(Route::Settings);  // Correct
router.push(Route::Setings);   // Compile error!

4. Protect Sensitive Routes

Use route guards for authentication.

5. Consider Deep Linking

Ensure routes can be entered directly, not just through navigation.


Summary

In this tutorial, you learned:


Previous: Part 11: Accessibility ←

Next: Part 13: Window Configuration →

In the next tutorial, we’ll explore window configuration - customizing window properties, multiple windows, and custom fonts.