Router
Router is a declarative routing system for Flet apps, inspired by React Router. It handles nested route matching, layout routes with outlets, dynamic segments, data loading, view-stack navigation, and provides hooks for accessing route state.
Why Router?
Without Router, multi-page Flet apps require manual route matching, conditional rendering, and parameter extraction. Router automates all of this:
- Nested routes — define parent/child hierarchies
- Layout routes — share layouts across pages using use_route_outlet()
- Dynamic segments —
:paramNamein paths, accessed via use_route_params() - Data loading — fetch data before rendering with
loader - Active link detection — highlight nav items with is_route_active()
- View-stack navigation — swipe-back and AppBar back button with
manage_views=True
Quick start
@ft.component
def Home():
return ft.Text("Welcome home!")
@ft.component
def About():
return ft.Text("About us")
@ft.component
def App():
return ft.Router([
ft.Route(index=True, component=Home),
ft.Route(path="about", component=About),
])
ft.run(lambda page: page.render(App))
See full example.
Defining routes
Routes are defined as a tree of Route objects.
Flat routes
All routes at the top level:
ft.Router([
ft.Route(index=True, component=Home),
ft.Route(path="about", component=About),
ft.Route(path="contact", component=Contact),
])
Nested routes
Child routes are nested under parent routes. The parent's path is automatically prepended:
ft.Route(
path="products",
component=ProductsList,
children=[
ft.Route(path=":pid", component=ProductDetails),
],
)
# /products → ProductsList
# /products/42 → ProductDetails (params: {"pid": "42"})
See full example.
Index routes
An index route (index=True) matches when the parent path matches exactly,
with no further segments:
ft.Route(path="settings", children=[
ft.Route(index=True, component=SettingsHome), # /settings
ft.Route(path="profile", component=Profile), # /settings/profile
])
Prefix routes
Pathless layout routes — group children under a shared layout without adding a path segment:
ft.Route(component=AdminLayout, children=[
ft.Route(path="users", component=Users), # /users
ft.Route(path="settings", component=Settings), # /settings
])
Path-only routes — add a path prefix without rendering a component:
ft.Route(path="api", children=[
ft.Route(path="users", component=ApiUsers), # /api/users
ft.Route(path="products", component=ApiProducts), # /api/products
])
Layout routes and use_route_outlet()
A layout route has both a component and children. Its component renders
shared UI (header, sidebar, footer) and calls use_route_outlet()
to place the matched child:
@ft.component
def AppLayout():
outlet = ft.use_route_outlet()
return ft.Column([
ft.AppBar(title=ft.Text("My App")),
ft.Container(content=outlet, expand=True),
ft.Text("Footer"),
])
ft.Router([
ft.Route(component=AppLayout, children=[
ft.Route(index=True, component=Home),
ft.Route(path="about", component=About),
]),
])
Layout routes can be nested to any depth. Each level calls use_route_outlet() to
get its immediate child. See full example.
Dynamic segments, optional segments, and splats
Route paths support Express-style patterns via the repath library.
Dynamic segments — :paramName matches a single path segment:
ft.Route(path="users/:userId", component=UserProfile)
# /users/42 → use_route_params() returns {"userId": "42"}
Optional segments — :paramName? may be absent:
ft.Route(path="users/:userId?", component=UserProfile)
# /users → {"userId": None}
# /users/42 → {"userId": "42"}
Splats (catch-all) — :paramName* matches zero or more segments:
ft.Route(path="files/:path*", component=FileBrowser)
# /files/a/b/c → {"path": "a/b/c"}
Custom regex — :paramName(\\d+) constrains a segment:
ft.Route(path="item/:id(\\d+)", component=ItemDetails)
# /item/42 → matches
# /item/abc → does NOT match
Navigation
Use page.navigate() from synchronous callbacks:
ft.Button("About", on_click=lambda: ft.context.page.navigate("/about"))
For async contexts, use page.push_route() with await.
The Router automatically re-renders when the route changes.
Active links with is_route_active()
is_route_active(path) returns True if the given path matches
the current location:
ft.Button(
"Products",
style=ft.ButtonStyle(
bgcolor=ft.Colors.PRIMARY_CONTAINER
if ft.is_route_active("/products")
else None,
),
on_click=lambda: ft.context.page.navigate("/products"),
)
By default it uses prefix matching — is_route_active("/products") returns
True for /products/42. Pass exact=True for exact matching.
See full example.
Data loading
Routes can define a loader function. It runs when the route matches and its
return value is available via use_route_loader_data():
def load_user(params):
return db.get_user(params["userId"])
ft.Route(path="users/:userId", component=UserProfile, loader=load_user)
@ft.component
def UserProfile():
user = ft.use_route_loader_data()
return ft.Text(f"Hello, {user.name}")
See full example.
Authentication
Auth is implemented using layout routes as guards — no special Router API needed.
Page redirect — redirect to /login if not authenticated:
@ft.component
def ProtectedRoute():
outlet = ft.use_route_outlet()
if not auth.is_authenticated:
ft.context.page.navigate("/login")
return ft.Text("Redirecting...")
return outlet
Role-based access — nest guard routes for layered access control:
ft.Router([
ft.Route(component=ProtectedRoute, children=[
ft.Route(index=True, component=Home),
ft.Route(component=AdminOnly, children=[
ft.Route(path="admin", component=AdminPanel),
]),
]),
])
404 handling
Pass a not_found component to render when no route matches:
@ft.component
def NotFound():
location = ft.use_route_location()
return ft.Text(f"Page not found: {location}")
ft.Router(routes, not_found=NotFound)
Multi-view navigation
By default, Router renders all route content within a single View.
With manage_views=True, the Router returns a list of Views — one per path level.
This enables:
- Swipe-back gesture on mobile
- System back button
- AppBar implicit back button
- Slide transition between route levels
Use page.render_views() instead of
page.render().
Components return Views
Each route component returns a View with route, appbar,
and controls:
@ft.component
def ProductDetails():
params = ft.use_route_params()
return ft.View(
route=f"/products/{params['pid']}",
appbar=ft.AppBar(title=ft.Text(f"Product #{params['pid']}")),
controls=[
ft.Text(f"Details for product #{params['pid']}"),
],
)
Nested route hierarchy
Structure routes as a nested tree. A pathless root ensures it is always in the view stack:
ft.Router([
ft.Route(component=Home, children=[
ft.Route(path="products", component=ProductsList, children=[
ft.Route(path=":pid", component=ProductDetails),
]),
]),
], manage_views=True)
/— 1 view (Home)/products— 2 views (Home, Products) — back button to Home/products/1— 3 views (Home, Products, Product Details) — back button to Products
See full example.
Shared layouts with outlet=True
Set outlet=True on a route to make it a layout that wraps child routes
via use_route_outlet(). The layout returns a
View; child components return regular controls:
@ft.component
def ProductsLayout():
outlet = ft.use_route_outlet()
location = ft.use_route_location()
return ft.View(
route=location,
appbar=ft.AppBar(title=ft.Text("Products")),
controls=[
ft.Container(content=outlet, expand=True),
ft.Text("Footer"),
],
)
ft.Route(path="products", component=ProductsLayout, outlet=True, children=[
ft.Route(component=ProductsList, children=[
ft.Route(path=":pid", component=ProductDetails),
]),
])
The layout's shared UI appears on every child route's View, and each child is still a separate View in the stack — back navigation works between them. See full example.
Avoiding transition animation
When switching between top-level NavigationRail destinations, set a fixed
route value on the root layout's View to prevent slide transitions:
@ft.component
def RootLayout():
outlet = ft.use_route_outlet()
return ft.View(
route="/", # fixed key — no animation between top-level pages
can_pop=False,
controls=[
ft.Row([
ft.NavigationRail(...),
ft.Container(content=outlet, expand=True),
], expand=True),
],
)
See full example.
Hooks reference
| Hook | Returns | Description |
|---|---|---|
| use_route_params() | dict[str, str] | All dynamic segment values from the matched route chain |
| use_route_location() | str | Current URL pathname |
| use_route_outlet() | component | Matched child route component (for layout routes) |
| use_route_loader_data() | Any | Return value of the current route's loader |
| is_route_active(path) | bool | Whether path matches the current location |