Skip to content
>GLB_
Go back

Building a multipage Streamlit app with st.navigation() — the modern way

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

Featurepages/ directoryst.navigation()
Page orderingalphabetical by filenameexplicit
Page display namesderived from filenamearbitrary
Shared initializationrepeated per pageonce in app.py
Dynamic pagesnot supportedsupported
URL path controllimitedvia url_path param
Entry pointStreamlit auto-picksyou 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.


Share this post:

Related Posts


Previous Post
Building a data quality dashboard on top of Tuva's DQI mart
Next Post
Connecting Streamlit to DuckDB: read-only mode and the lock problem