Skip to content

Navigation and Routing

Navigation and routing is the core of building multi-screen Flet apps. It lets you organize your UI into virtual pages (View objects), keep URL/history in sync, and support deep links to specific app states.

This page focuses on the current routing model and maintained examples.

Routing model in Flet#

A Page is a container of views (page.views), where each view represents one route-level screen.

A reliable setup uses a single source of truth: derive page.views from page.route.

Route basics#

The default route is / when no route is provided.

import flet as ft


def main(page: ft.Page):
    page.add(ft.Text(f"Initial route: {page.route}"))


if __name__ == "__main__":
    ft.run(main)

All routes should start with /, for example /store, /products/42, /settings/mail.

Handling route changes#

Whenever route changes (URL edit, browser Back/Forward, or app navigation), page.on_route_change event is triggered. Use this event as the place where you decide which views must exist for the current route.

import flet as ft


def main(page: ft.Page):
    page.add(ft.Text(f"Initial route: {page.route}"))

    def route_change(e):
        page.add(ft.Text(f"New route: {e.route}"))

    page.on_route_change = route_change
    page.update()


if __name__ == "__main__":
    ft.run(main)

Building views from route#

The pattern below is the baseline for most apps:

  1. Clear page.views.
  2. Add root view (/).
  3. Add extra views conditionally based on page.route.
  4. Handle Back in page.on_view_pop and navigate to the new top view.

Why is this pattern important?

  • Keeps URL, history stack, and visible UI synchronized.
  • Supports deep links and reloads naturally.
  • Makes navigation deterministic and easier to debug.
import flet as ft


def main(page: ft.Page):
    page.title = "Routes Example"

    print("Initial route:", page.route)

    async def open_mail_settings(e):
        await page.push_route("/settings/mail")

    async def open_settings(e):
        await page.push_route("/settings")

    def route_change():
        print("Route change:", page.route)
        page.views.clear()
        page.views.append(
            ft.View(
                route="/",
                controls=[
                    ft.AppBar(title=ft.Text("Flet app")),
                    ft.Button("Go to settings", on_click=open_settings),
                ],
            )
        )
        if page.route == "/settings" or page.route == "/settings/mail":
            page.views.append(
                ft.View(
                    route="/settings",
                    controls=[
                        ft.AppBar(
                            title=ft.Text("Settings"),
                            bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                        ),
                        ft.Text("Settings!", theme_style=ft.TextThemeStyle.BODY_MEDIUM),
                        ft.Button(
                            content="Go to mail settings",
                            on_click=open_mail_settings,
                        ),
                    ],
                )
            )
        if page.route == "/settings/mail":
            page.views.append(
                ft.View(
                    route="/settings/mail",
                    controls=[
                        ft.AppBar(
                            title=ft.Text("Mail Settings"),
                            bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                        ),
                        ft.Text("Mail settings!"),
                    ],
                )
            )
        page.update()

    async def view_pop(e):
        if e.view is not None:
            print("View pop:", e.view)
            page.views.remove(e.view)
            top_view = page.views[-1]
            await page.push_route(top_view.route)

    page.on_route_change = route_change
    page.on_view_pop = view_pop

    route_change()


if __name__ == "__main__":
    ft.run(main)

Programmatic navigation#

Use page.push_route() to navigate.

You can also pass query parameters as keyword arguments:

await page.push_route("/search", q="flet", page=2)

Back navigation and pop confirmation#

When users go back, Flet triggers page.on_view_pop. For flows requiring confirmation (for example, unsaved changes), disable automatic pop and confirm manually with View.can_pop + View.on_confirm_pop.

import flet as ft


def main(page: ft.Page):
    page.title = "Routes Example"

    def route_change():
        page.views.clear()
        page.views.append(MainView("/"))
        if page.route == "/store":
            page.views.append(PermissionView("/store"))
        page.update()

    async def view_pop(e: ft.ViewPopEvent):
        if e.view is not None:
            print("View pop:", e.view)
            page.views.remove(e.view)
            top_view = page.views[-1]
            await page.push_route(top_view.route)

    page.on_route_change = route_change
    page.on_view_pop = view_pop

    route_change()


class MainView(ft.View):
    def __init__(self, path):
        super().__init__(
            route=path,
            appbar=ft.AppBar(title=ft.Text("Flet app")),
            controls=[
                ft.Button("Go to store", on_click=self.open_store),
            ],
        )

    async def open_store(self, e):
        await self.page.push_route("/store")


class PermissionView(ft.View):
    def __init__(self, path):
        super().__init__(
            route=path,
            appbar=ft.AppBar(title=ft.Text(f"{path} View")),
            can_pop=False,
            on_confirm_pop=self.ask_pop_permission,
        )

    async def ask_pop_permission(self, e):
        async def on_dlg_yes(e):
            self.page.pop_dialog()
            await self.confirm_pop(True)

        async def on_dlg_no(e):
            self.page.pop_dialog()
            await self.confirm_pop(False)

        dlg_modal = ft.AlertDialog(
            title=ft.Text("Please confirm"),
            content=ft.Text("Go home?"),
            actions=[
                ft.TextButton(
                    "Yes",
                    on_click=on_dlg_yes,
                ),
                ft.TextButton(
                    "No",
                    on_click=on_dlg_no,
                ),
            ],
            actions_alignment=ft.MainAxisAlignment.END,
            on_dismiss=lambda e: print("Modal dialog dismissed!"),
        )

        self.page.show_dialog(dlg_modal)
        # await self.confirm_pop(True)


if __name__ == "__main__":
    ft.run(main)

Routing composes well with navigation controls such as drawer, rail, and tabs. This example shows route-driven drawer navigation with multiple top-level destinations:

import flet as ft


def main(page: ft.Page):
    page.title = "Drawer navigation"

    async def handle_change(e):
        if e.control.selected_index == 0:
            await page.push_route("/")
        elif e.control.selected_index == 1:
            await page.push_route("/store")
        elif e.control.selected_index == 2:
            await page.push_route("/about")

    def create_drawer(selected_index=0):
        return ft.NavigationDrawer(
            selected_index=selected_index,
            on_change=handle_change,
            controls=[
                ft.Container(height=12),
                ft.NavigationDrawerDestination(
                    label="Home",
                    icon=ft.Icons.HOME_OUTLINED,
                    selected_icon=ft.Icon(ft.Icons.HOME),
                ),
                ft.Divider(thickness=2),
                ft.NavigationDrawerDestination(
                    label="Store",
                    icon=ft.Icon(ft.Icons.STORE_OUTLINED),
                    selected_icon=ft.Icon(ft.Icons.STORE),
                ),
                ft.NavigationDrawerDestination(
                    label="About",
                    icon=ft.Icon(ft.Icons.PHONE_OUTLINED),
                    selected_icon=ft.Icons.PHONE,
                ),
            ],
        )

    async def show_drawer():
        await page.show_drawer()

    def route_change(route):
        page.views.clear()
        page.views.append(
            ft.View(
                route="/",
                controls=[
                    ft.AppBar(
                        title=ft.Text("Home", expand=True),
                        bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                        leading=ft.IconButton(ft.Icons.MENU, on_click=show_drawer),
                    ),
                    ft.Text("Welcome to Home Page"),
                ],
                drawer=create_drawer(selected_index=0)
                if page.route == "/"
                else None,  # add drawer only if home page is shown
            )
        )

        if page.route == "/store":
            page.views.append(
                ft.View(
                    route="/store",
                    controls=[
                        ft.AppBar(
                            title=ft.Text("Store", expand=True),
                            bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                            leading=ft.IconButton(ft.Icons.MENU, on_click=show_drawer),
                            automatically_imply_leading=False,
                        ),
                        ft.Text("Welcome to Store Page"),
                        ft.Button("Go About", on_click=lambda _: page.go("/about")),
                    ],
                    drawer=create_drawer(selected_index=1),
                )
            )

        if page.route == "/about":
            page.views.append(
                ft.View(
                    route="/about",
                    controls=[
                        ft.AppBar(
                            title=ft.Text("About", expand=True),
                            bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                            leading=ft.IconButton(ft.Icons.MENU, on_click=show_drawer),
                            automatically_imply_leading=False,
                        ),
                        ft.Text("Welcome to About Page"),
                        ft.Button("Go Store", on_click=lambda _: page.go("/store")),
                    ],
                    drawer=create_drawer(selected_index=2),
                )
            )

    async def view_pop(view):
        page.views.pop()
        top_view = page.views[-1]
        await page.push_route(top_view.route)

    page.on_route_change = route_change
    page.on_view_pop = view_pop
    route_change(page.route)


if __name__ == "__main__":
    ft.run(main)

Route templates (parameterized routes)#

Use TemplateRoute to match and parse route parameters, for example /books/:id. Template syntax is provided by repath.

import flet as ft

troute = ft.TemplateRoute(page.route)

if troute.match("/books/:id"):
    print("Book ID:", troute.id)
elif troute.match("/account/:account_id/orders/:order_id"):
    print("Account:", troute.account_id, "Order:", troute.order_id)
else:
    print("Unknown route")

Web URL strategy#

Flet web apps support two URL strategies :

  • "path" (default): in the form https://myapp.dev/store
  • "hash": https://myapp.dev/#/store

It can be set via route_url_strategy in ft.run()

import flet as ft
ft.run(main, route_url_strategy="hash")

For Flet server deployments, you can also set the FLET_ROUTE_URL_STRATEGY environment variable.

Practical recommendations#

  • Always keep a root / view in page.views.
  • Keep route handling centralized in page.on_route_change; avoid mutating page.views from many places.
  • When adding new routes, test these cases: direct deep link, browser Back/Forward, app Back button, and reload.
  • Use route templates for dynamic segments instead of manual string splitting.