Routing

Routes map a URL pattern to a layout callback. The same registry drives a desktop app's view-switching and a web build's HTTP endpoints, so a „settings page“ written once shows up at the /settings URL on the web and as a separate top-level layout on desktop.

Registering routes

Register routes on the AppConfig before passing it to App::create:

use azul::prelude::*;

extern "C" fn layout_home(_: &mut RefAny, _: LayoutCallbackInfo) -> StyledDom { /* ... */ todo!() }
extern "C" fn layout_user(_: &mut RefAny, _: LayoutCallbackInfo) -> StyledDom { /* ... */ todo!() }
extern "C" fn layout_settings(_: &mut RefAny, _: LayoutCallbackInfo) -> StyledDom { /* ... */ todo!() }

fn main() {
    let mut config = AppConfig::create();
    config.add_route("/", layout_home);
    config.add_route("/user/:id", layout_user);
    config.add_route("/settings", layout_settings);

    let app = App::create(initial_data, config);
    app.run(WindowCreateOptions::new(layout_home));
}

Adding a route that already exists (same pattern) replaces the previous registration. The first registered route — or the explicit "/" if present — is the initial layout.

Pattern syntax

Patterns are slash-separated segments. Each segment is either:

  • A literal — matches that exact path component.
  • A :name placeholder — matches any path component and captures it as a parameter named name.
Pattern Path Match
"/" "/" yes; no params
"/about" "/about" yes; no params
"/about" "/settings" no
"/user/:id" "/user/42" yes; id = "42"
"/user/:id" "/user/42/edit" no (segment count mismatch)
"/post/:slug" "/post/hello-world" yes; slug = "hello-world"

Patterns are matched in registration order; the first match wins.

Reading the active route

Inside a LayoutCallback, the LayoutCallbackInfo knows which route triggered the call:

extern "C" fn layout_user(data: &mut RefAny, info: LayoutCallbackInfo) -> StyledDom {
    let id = info.get_route_param("id").map(|s| s.as_str()).unwrap_or("");
    let title = format!("User #{}", id);
    Dom::create_h1_with_text(title).style_dom()
}

info.get_active_route() returns the full RouteMatch (pattern + all params); info.get_route_param(key) is the convenience reader for one param.

Inside an event CallbackInfo, the same data is reachable via info.get_route_pattern() (the active pattern) and info.get_route_param(key) (one param). The set_route_param(key, value) helper modifies a param in place — useful for paginated views that want to bump ?page=2 without a full route switch:

extern "C" fn next_page(data: RefAny, mut info: CallbackInfo) -> Update {
    let cur: u32 = info.get_route_param("page".into()).as_str()
        .parse().unwrap_or(1);
    info.set_route_param("page".into(), (cur + 1).to_string().into());
    Update::DoNothing  // set_route_param already triggers a refresh
}

On web, set_route_param calls history.replaceState() so the URL in the address bar stays in sync without adding a history entry.

Switching routes from a callback

CallbackInfo::switch_route is the imperative form — used when a button or menu item should navigate elsewhere:

extern "C" fn open_settings(_: RefAny, mut info: CallbackInfo) -> Update {
    info.switch_route("/settings".into(), StringPairVec::new());
    Update::RefreshDom
}

extern "C" fn open_user(data: RefAny, mut info: CallbackInfo) -> Update {
    let id = match data.downcast_ref::<u64>() { Some(i) => *i, None => return Update::DoNothing };
    let params = vec![StringPair { key: "id".into(), value: id.to_string().into() }].into();
    info.switch_route("/user/:id".into(), params);
    Update::RefreshDom
}

The framework swaps the active layout callback on the next frame, fires RefreshDom, and reconciles the new tree against the previous one — focus, scroll, and dataset state migrate across matched nodes the same way a RefreshDom from an in-place mutation does.

On web, switch_route calls history.pushState() so the back button works as users expect.

A practical multi-route layout

A typical app keeps each top-level view in its own callback and shares a model:

use azul::prelude::*;

struct AppModel {
    users: Vec<User>,
    current_filter: String,
}

extern "C" fn layout_home(data: &mut RefAny, info: LayoutCallbackInfo) -> StyledDom {
    let model = data.downcast_ref::<AppModel>().unwrap();
    Dom::create_body()
        .with_child(navbar(/* current_route */ info.get_active_route()))
        .with_child(home_content(&model))
        .style_dom()
}

extern "C" fn layout_user(data: &mut RefAny, info: LayoutCallbackInfo) -> StyledDom {
    let model = data.downcast_ref::<AppModel>().unwrap();
    let id = info.get_route_param("id").map(|s| s.as_str()).unwrap_or("");
    let user = model.users.iter().find(|u| u.id == id);

    let body = match user {
        Some(u) => user_detail(u),
        None => not_found_page(id),
    };
    Dom::create_body()
        .with_child(navbar(info.get_active_route()))
        .with_child(body)
        .style_dom()
}

extern "C" fn layout_settings(data: &mut RefAny, info: LayoutCallbackInfo) -> StyledDom {
    let model = data.downcast_ref::<AppModel>().unwrap();
    Dom::create_body()
        .with_child(navbar(info.get_active_route()))
        .with_child(settings_panel(&model))
        .style_dom()
}

fn main() {
    let mut config = AppConfig::create();
    config.add_route("/", layout_home);
    config.add_route("/user/:id", layout_user);
    config.add_route("/settings", layout_settings);

    let app = App::create(RefAny::new(initial_model()), config);
    app.run(WindowCreateOptions::new(layout_home));
}

Pull the navbar into its own component (with add_component_library or a regular function) so the active-link styling — typically a .is-active class on the link whose href matches the current route — only lives in one place.

Web vs desktop

On a desktop build, the route is purely an in-memory selector for which layout callback to run. There's no URL bar, no history stack, no window.location — the route is application state. switch_route updates that state and triggers a reconcile.

On a web build (compiled to WASM and served through azul's web host), each registered route also maps to an HTTP endpoint on the server side: a request to /user/42 runs layout_user with the extracted params and returns the rendered HTML, so the page is SEO-readable on first load. switch_route then calls history.pushState() for the in-page client-side transition; set_route_param calls history.replaceState(). The same callback code drives both the server-rendered first-load HTML and the client-side updates.

See Web Deployment for the WASM-build pipeline, the static asset layout, and how the web host serves routes.

Coming Up Next

  • DOM — Tree shape, node constructors, the cascade
  • Callbacksswitch_route, get_route_param, and the rest of CallbackInfo
  • Web Deployment — Routes as HTTP endpoints

Back to guide index