Real apps grow beyond one screen; structured navigation keeps code and UI organized.
The recommended way to build multipage apps is an entrypoint file that defines pages and calls st.navigation:
pg = st.navigation([
st.Page("pages/dashboard.py", title="Dashboard", icon=":material/dashboard:"),
st.Page("pages/settings.py", title="Settings"),
])
pg.run()st.Page accepts a file path or a function, so small pages can live in one file. Pass a dict to group pages into sections: {"Reports": [...], "Admin": [...]}. position="top" puts navigation in a top bar instead of the sidebar. default=True marks the home page, and url_path= sets the page's URL.
The entrypoint runs on every rerun before the page, so it is the place for shared things: st.set_page_config, login checks, common sidebar widgets and building the page list. Because the list is built in code, you can show admin pages only to admins.
Move between pages with st.page_link(page, label=...) (a link) or st.switch_page(page) (programmatic, for example after a form submits).
Session state is shared across pages within a session, so a selection made on one page can be used on another. Widget values themselves are cleaned up when their page is not shown; copy what you need into your own state keys.
The older convention, a pages/ directory next to the main script that Streamlit turns into pages automatically, still works but offers less control.
Example
# streamlit_app.py (entrypoint)
import streamlit as st
def home():
st.title("Home")
st.page_link(settings, label="Go to settings", icon=":material/settings:")
def settings():
st.title("Settings")
st.session_state.theme = st.radio("Theme", ["light", "dark"])
def admin():
st.title("Admin")
home_page = st.Page(home, title="Home", icon=":material/home:", default=True)
settings = st.Page(settings, title="Settings")
pages = {"App": [home_page, settings]}
if st.session_state.get("is_admin"): # pages can depend on the user
pages["Admin"] = [st.Page(admin, title="Admin")]
st.set_page_config(page_title="My app")
pg = st.navigation(pages, position="sidebar")
pg.run()Remember
- Define pages with
st.Pageand run the selected one viast.navigation(...).run() - The entrypoint runs every rerun: put shared config, auth and widgets there
- Navigate with
st.page_linkorst.switch_page; session state is shared across pages
Watch out for
- Forgetting to call
.run()on the page returned byst.navigation, so no page content appears.