Before Streamlit 1.31, multipage apps worked by placing .py files in a pages/ directory and letting Streamlit auto-discover them. It worked, but gave you no control over page ordering, naming, access control, or shared state initialization.
st.navigation() is the replacement. It’s explicit, composable, and lets you control everything about how pages are structured and when they render.
The basic pattern
Your entry point is app.py. It defines the pages and runs the selected one:
import streamlit as st
pg = st.navigation([
st.Page("tuva_explorer.py", title="Tuva Explorer"),
st.Page("data_quality_explorer.py", title="Data Quality"),
st.Page("predictions.py", title="Risk Predictions"),
])
pg.run()
Run it with:
streamlit run app.py
Streamlit renders a sidebar with the page list. Clicking a page causes pg.run() to execute the corresponding file.
What st.Page accepts
st.Page(
page, # str (relative file path) or callable
title=None, # display name in sidebar nav
icon=None, # emoji string or Material icon name
url_path=None, # custom URL slug, e.g. "data-quality"
default=False, # whether this is the landing page
)
The page argument is a file path relative to app.py, or a callable if you want to define pages inline as functions. For a multi-file app, the file path form is cleaner.
Shared initialization in app.py
One of the main benefits over the pages/ convention: you can run initialization logic in app.py before any page renders. This runs on every page navigation:
import streamlit as st
import duckdb
if "db" not in st.session_state:
st.session_state.db = duckdb.connect("tuva.duckdb", read_only=True)
pg = st.navigation([
st.Page("tuva_explorer.py", title="Tuva Explorer"),
st.Page("data_quality_explorer.py", title="Data Quality"),
st.Page("predictions.py", title="Risk Predictions"),
])
pg.run()
With the old pages/ convention, you had to repeat this initialization in every page file or import from a shared helper. Now it’s centralized. The DuckDB connection is created once and stored in st.session_state where every page can access it.
What a page file looks like
Each page file is a standard Python script. It reads shared state from st.session_state and renders with Streamlit calls:
# tuva_explorer.py
import streamlit as st
import pandas as pd
con = st.session_state.db
st.title("Tuva Explorer")
@st.cache_data
def load_patient_summary():
return con.execute("""
SELECT
patient_id,
patient_age,
patient_sex,
total_paid_amount,
suspecting_gaps
FROM hcc_suspecting__summary
ORDER BY suspecting_gaps DESC
LIMIT 500
""").df()
st.dataframe(load_patient_summary())
Dynamic page lists
st.navigation() accepts a plain list, so you can filter it based on state:
pages = [
st.Page("tuva_explorer.py", title="Tuva Explorer"),
st.Page("data_quality_explorer.py", title="Data Quality"),
]
if st.session_state.get("predictions_enabled", True):
pages.append(st.Page("predictions.py", title="Risk Predictions"))
pg = st.navigation(pages)
pg.run()
This pattern works for feature flags, role-based access, or showing pages only after some prerequisite step is complete.
Compared to the pages/ convention
| Feature | pages/ directory | st.navigation() |
|---|---|---|
| Page ordering | alphabetical by filename | explicit |
| Page display names | derived from filename | arbitrary |
| Shared initialization | repeated per page | once in app.py |
| Dynamic pages | not supported | supported |
| URL path control | limited | via url_path param |
| Entry point | Streamlit auto-picks | you define |
The pages/ convention still works and isn’t deprecated. But for any app with more than two pages, shared state, or any need to control ordering or access, st.navigation() is the right API to use. Streamlit 1.57 (used in this project) includes it with no additional install.