lib/streamlit/.agents/skills/developing-with-streamlit/references/multipage-apps.md
Structure and navigation for apps with multiple pages.
streamlit_app.py # Main entry point
app_pages/
home.py
analytics.py
settings.py
Important: Name your pages directory app_pages/ (not pages/). Using pages/ conflicts with Streamlit's old auto-discovery API and can cause unexpected behavior.
# streamlit_app.py
import streamlit as st
# Initialize user-specific state (shared across pages)
if "selected_project" not in st.session_state:
st.session_state.selected_project = None
# Define navigation
page = st.navigation([
st.Page("app_pages/home.py", title="Home", icon=":material/home:"),
st.Page("app_pages/analytics.py", title="Analytics", icon=":material/bar_chart:"),
st.Page("app_pages/settings.py", title="Settings", icon=":material/settings:"),
])
# App-level UI runs before page content
# Useful for shared elements like titles
st.title(f"{page.icon} {page.title}")
page.run()
Note: When you handle titles in streamlit_app.py, individual pages should NOT use st.title again.
Few pages (3-7) → Top navigation:
page = st.navigation([...], position="top")
Creates a clean horizontal menu. Great for simple apps. Sections are supported too—they appear as dropdowns in the top nav.
Many pages or nested sections → Sidebar:
page = st.navigation({
"Main": [
st.Page("app_pages/home.py", title="Home"),
st.Page("app_pages/analytics.py", title="Analytics"),
],
"Admin": [
st.Page("app_pages/settings.py", title="Settings"),
st.Page("app_pages/users.py", title="Users"),
],
}, position="sidebar")
Mixed: Some pages ungrouped:
Use an empty string key "" for pages that shouldn't be in a section. These ungrouped pages always appear first, before any named groups. Put all ungrouped pages in a single "" key:
page = st.navigation({
"": [
st.Page("app_pages/home.py", title="Home"),
st.Page("app_pages/about.py", title="About"),
],
"Analytics": [
st.Page("app_pages/dashboard.py", title="Dashboard"),
st.Page("app_pages/reports.py", title="Reports"),
],
}, position="top")
# app_pages/analytics.py
import streamlit as st
# Access shared state
project = st.session_state.selected_project
user = st.session_state.user
# Page-specific content (title is handled in streamlit_app.py)
data = fetch_analytics(user.id, project)
st.line_chart(data)
Initialize state in the main module only if it's needed across multiple pages:
# streamlit_app.py
# Shared resources — use @st.cache_resource, NOT session_state
@st.cache_resource
def get_api_client():
return init_client()
# User-specific state — use session_state
st.session_state.user = get_user()
st.session_state.settings = load_settings()
Why main module (for global state):
Use @st.cache_resource for shared resources (API clients, DB connections, ML models). Use st.session_state only for per-user data (selections, form inputs, preferences).
Use prefixed keys for page-specific state:
# app_pages/analytics.py
if "analytics_date_range" not in st.session_state:
st.session_state.analytics_date_range = default_range()
Show different pages based on user role, authentication, or any other condition by building the pages list dynamically:
# streamlit_app.py
import streamlit as st
pages = [st.Page("app_pages/home.py", title="Home", icon=":material/home:")]
if st.user.is_logged_in:
pages.append(st.Page("app_pages/dashboard.py", title="Dashboard", icon=":material/bar_chart:"))
if st.session_state.get("is_admin"):
pages.append(st.Page("app_pages/admin.py", title="Admin", icon=":material/settings:"))
page = st.navigation(pages)
page.run()
Common conditions for showing/hiding pages:
st.user.is_logged_in for authenticated usersst.session_state flags (roles, permissions, feature flags)When importing from page files in app_pages/, always import from the root directory perspective:
# app_pages/dashboard.py - GOOD
from utils.data import load_sales_data
# app_pages/dashboard.py - BAD (don't use relative imports)
from ..utils.data import load_sales_data