Navigation¶
Small examples that show the three navigators (stack, tab, and drawer),
how screens receive their params as ordinary arguments, and how
navigators nest. Every screen is a @pn.component, and every navigator
is a module-level value rendered by
NavigationContainer.
For the conceptual model and the full API, see the Navigation guide and the Navigation API reference.
Run it¶
Create a project with pn init navigation-demo, then cd navigation-demo.
Replace app/main.py with the Stack, Tab, Drawer, or Nesting snippet
below. Each includes its imports and defines App, the project's root
component.
From the project root, run pn preview to open the app in your browser.
To run on a device or simulator, leave the preview running and use
pn run android or pn run ios in another terminal from the same
directory. See the Browser preview guide
and Development workflow for more options.
Stack navigator¶
A pushable, poppable stack: the default for "go from screen A to screen B with a back button."
import pythonnative as pn
@pn.component
def HomeScreen() -> pn.Node:
nav = pn.use_navigation()
return pn.Column(
pn.Text("Home", style={"font_size": 28, "bold": True}),
pn.Button("View profile", on_press=lambda: nav.push(ProfileScreen(user_id=42))),
style={"gap": 12, "padding": 16},
)
@pn.component
def ProfileScreen(user_id: int) -> pn.Node:
nav = pn.use_navigation()
pn.use_screen_options(title=f"User {user_id}")
return pn.Column(
pn.Text(f"User #{user_id}", style={"font_size": 24}),
pn.Button("Back", on_press=nav.go_back),
style={"gap": 12, "padding": 16},
)
Root = pn.StackNavigator(
pn.Screen(HomeScreen, title="Home"),
ProfileScreen,
)
@pn.component
def App() -> pn.Node:
return pn.NavigationContainer(Root)
ProfileScreen(user_id=42) is an ordinary call, so your editor
autocompletes user_id and your type checker rejects a missing or
misspelled param. nav.push(...) always adds a new screen,
nav.navigate(...) returns to an existing ProfileScreen if there is
one, and nav.go_back() pops one screen. To replace the entire history
(after signing in, for example), use nav.reset(HomeScreen()).
Tab navigator¶
A persistent native tab bar, with one screen per tab. Each tab keeps its own state across switches.
import pythonnative as pn
@pn.component
def FeedScreen() -> pn.Node:
return pn.Text("Feed", style={"padding": 16, "font_size": 24})
@pn.component
def SearchScreen() -> pn.Node:
query, set_query = pn.use_state("")
return pn.Column(
pn.TextInput(value=query, on_change=set_query, placeholder="Search..."),
pn.Text(f"Results for: {query}"),
style={"gap": 12, "padding": 16},
)
@pn.component
def SettingsScreen() -> pn.Node:
return pn.Text("Settings", style={"padding": 16, "font_size": 24})
Tabs = pn.TabNavigator(
pn.Screen(FeedScreen, title="Feed", tab_bar_icon="house"),
pn.Screen(SearchScreen, title="Search", tab_bar_icon="search"),
pn.Screen(SettingsScreen, title="Settings", tab_bar_icon="settings"),
)
@pn.component
def App() -> pn.Node:
return pn.NavigationContainer(Tabs)
The search box keeps its query when the user switches to Settings
and back: tab screens stay mounted unless a screen sets
unmount_on_blur=True. Tab screens must be callable without arguments,
so give any tab parameters defaults.
Drawer navigator¶
A side drawer for primary navigation in larger apps.
import pythonnative as pn
from pythonnative.navigation import DrawerNavigation
@pn.component
def MenuButton() -> pn.Node:
nav = pn.use_navigation()
assert isinstance(nav, DrawerNavigation)
return pn.Button("Menu", on_press=nav.toggle_drawer)
@pn.component
def InboxScreen() -> pn.Node:
return pn.Column(
MenuButton(),
pn.Text("Inbox", style={"font_size": 24}),
style={"gap": 12, "padding": 16},
)
@pn.component
def SentScreen() -> pn.Node:
return pn.Column(
MenuButton(),
pn.Text("Sent", style={"font_size": 24}),
style={"gap": 12, "padding": 16},
)
Drawer = pn.DrawerNavigator(
pn.Screen(InboxScreen, title="Inbox"),
pn.Screen(SentScreen, title="Sent"),
)
@pn.component
def App() -> pn.Node:
return pn.NavigationContainer(Drawer)
The drawer is drawn in Python and has no navigation bar of its own, so
each screen renders its own Menu button. The drawer opens with a
swipe from the leading edge or with toggle_drawer() on the
DrawerNavigation handle.
use_navigation() is typed as the base Navigation, so the
isinstance check narrows it to the drawer handle.
Nesting¶
Navigators compose: a navigator is a valid screen of another navigator. A common shape is a root stack whose first screen is a tab navigator, so detail screens push over the tab bar:
import pythonnative as pn
@pn.component
def PostLink(id: int) -> pn.Node:
nav = pn.use_navigation()
return pn.Button(f"Post {id}", on_press=lambda: nav.push(PostScreen(id=id)))
@pn.component
def FeedScreen() -> pn.Node:
return pn.Column(
*(PostLink(id=post_id).with_key(post_id) for post_id in range(1, 4)),
style={"gap": 8, "padding": 16},
)
@pn.component
def ProfileScreen(user: str = "me") -> pn.Node:
return pn.Text(f"Profile: {user}", style={"padding": 16, "font_size": 24})
@pn.component
def PostScreen(id: int) -> pn.Node:
nav = pn.use_navigation()
pn.use_screen_options(title=f"Post {id}")
return pn.Column(
pn.Text(f"Post #{id} by ada"),
pn.Button("View author", on_press=lambda: nav.navigate(ProfileScreen(user="ada"))),
style={"gap": 12, "padding": 16},
)
Tabs = pn.TabNavigator(
pn.Screen(FeedScreen, title="Feed", tab_bar_icon="house", path="feed"),
pn.Screen(ProfileScreen, title="Profile", tab_bar_icon="user", path="u/{user}"),
name="Main",
)
Root = pn.StackNavigator(
pn.Screen(Tabs, header_shown=False),
pn.Screen(PostScreen, path="posts/{id}"),
)
@pn.component
def App() -> pn.Node:
return pn.NavigationContainer(Root, link_prefixes=["navdemo://"])
nav.push(PostScreen(id=...)) from the feed tab reaches the root
stack, because a target the tab navigator doesn't know is handed to its
parent. View author navigates back down: the root stack finds
ProfileScreen inside Tabs, pops the post, and selects the profile
tab with user="ada". The same static tree answers deep links, so
navdemo://posts/2 opens PostScreen(id=2) above the feed and
navdemo://u/ada opens the profile tab.
Focus-aware effects¶
When you need to run something only while a screen is visible (a
camera, GPS, or an animation), use
use_focus_effect:
from collections.abc import Callable
@pn.component
def CameraScreen() -> pn.Node:
def start() -> Callable[[], None]:
start_camera()
return stop_camera
pn.use_focus_effect(start, [])
return pn.View()
The cleanup runs as soon as the user navigates away, even if the screen stays mounted.
To try this with the Tab navigator example, add CameraScreen and
stand-ins for start_camera and stop_camera, and replace
pn.Screen(SettingsScreen, ...) with
pn.Screen(CameraScreen, title="Camera", tab_bar_icon="camera"). Run
pn preview, then switch between the Camera and Feed tabs to trigger
the effect and its cleanup.
Next steps¶
- Reference: Navigation API.
- Patterns and lifecycle: Navigation guide.
- Render lists inside a tab: Lists.