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.
page.routeis the current route string (for example/,/store,/settings/mail).page.viewsis the active navigation stack.page.on_route_changerebuilds the stack when route changes.page.on_view_pophandles Back navigation (system Back, AppBar Back, browser Back).
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:
- Clear
page.views. - Add root view (
/). - Add extra views conditionally based on
page.route. - Handle Back in
page.on_view_popand 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:
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)
Navigation UI patterns#
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 formhttps://myapp.dev/store"hash":https://myapp.dev/#/store
It can be set via route_url_strategy in ft.run()
For Flet server deployments, you can also set the FLET_ROUTE_URL_STRATEGY
environment variable.
Practical recommendations#
- Always keep a root
/view inpage.views. - Keep route handling centralized in
page.on_route_change; avoid mutatingpage.viewsfrom 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.