Single-page dashboards get cluttered fast. Today you'll structure your app into multiple pages using Streamlit's native multi-page system, build a proper sidebar, and use session state to share data between pages.
Streamlit has a built-in multi-page system. Create a pages/ folder next to your main script and add Python files. Each file becomes a page automatically.
my_dashboard/
├── app.py ← entry point (home page)
├── pages/
│ ├── 1_Sales.py ← automatically becomes "Sales" page
│ ├── 2_Products.py ← becomes "Products" page
│ └── 3_Settings.py ← becomes "Settings" page
├── .streamlit/
│ └── secrets.toml
└── sales.db
The number prefix controls the sort order. Underscores become spaces in the sidebar nav. Streamlit creates the sidebar navigation automatically.
import streamlit as st
st.set_page_config(
page_title="Sales Dashboard",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded"
)
st.title("Sales Dashboard")
st.write("Welcome. Use the sidebar to navigate.")
# Show summary cards on the home page
col1, col2, col3, col4 = st.columns(4)
col1.metric("Total Revenue", "$2.4M", "+12%")
col2.metric("Deals Closed", "847", "+23")
col3.metric("Avg Order", "$2,836", "-3%")
col4.metric("Active Regions", "4", "0")
Use st.sidebar to add content to the sidebar that appears on every page — like filters that apply globally.
import streamlit as st
import pandas as pd
import plotly.express as px
from sqlalchemy import create_engine
engine = create_engine("sqlite:///sales.db")
# Sidebar filters — shows up in the sidebar
with st.sidebar:
st.header("Filters")
regions = pd.read_sql("SELECT DISTINCT region FROM sales", engine)["region"].tolist()
selected_regions = st.multiselect("Regions", regions, default=regions)
date_range = st.date_input(
"Date Range",
value=("2024-01-01", "2024-03-31")
)
# Load and filter data
@st.cache_data(ttl=300)
def load_data():
return pd.read_sql("SELECT * FROM sales", engine, parse_dates=["date"])
df = load_data()
df = df[df["region"].isin(selected_regions)]
if len(date_range) == 2:
df = df[(df["date"] >= str(date_range[0])) & (df["date"] <= str(date_range[1]))]
# Page content
st.title("Sales Analysis")
st.caption(f"{len(df):,} records | {selected_regions}")
fig = px.bar(
df.groupby("region")["revenue"].sum().reset_index(),
x="region", y="revenue", title="Revenue by Region"
)
st.plotly_chart(fig, use_container_width=True)
Each page reruns independently. If you want a filter set on one page to persist when the user navigates, use st.session_state.
# Initialize state with a default
if "selected_region" not in st.session_state:
st.session_state.selected_region = "All"
# Widget that writes to session state
region = st.selectbox(
"Region",
["All", "North", "South", "East", "West"],
key="selected_region" # automatically reads/writes session state
)
# Read state anywhere (including other pages)
st.write(f"Current region: {st.session_state.selected_region}")
# Manually update state
if st.button("Reset to All Regions"):
st.session_state.selected_region = "All"
st.rerun() # force a rerun to reflect the change
# Must be the FIRST Streamlit call on each page
st.set_page_config(
page_title="Products — Sales Dashboard",
page_icon="📦",
layout="wide" # "centered" is the default
)
# Customize sidebar
st.sidebar.image("logo.png", width=120)
st.sidebar.markdown("---")
st.sidebar.caption("Data refreshes every 5 minutes")
layout="wide" uses the full browser width. Use it for dashboards with multiple charts side by side. Use "centered" for text-heavy pages.Streamlit 1.23+ supports editable tables with st.data_editor(). Users can edit cells and you get the modified dataframe back.
st.subheader("Sales Records")
# Read-only table
st.dataframe(df, use_container_width=True)
# Editable table
edited_df = st.data_editor(
df[["date", "region", "product", "revenue"]],
use_container_width=True,
num_rows="dynamic" # allows adding/deleting rows
)
if st.button("Save Changes"):
# edited_df contains the modified version
st.success(f"Saved {len(edited_df)} rows")
app.py + pages/1_Sales.py + pages/2_Products.py.st.set_page_config(layout="wide") on all three files.st.session_state so it persists when navigating.streamlit run app.py and verify the sidebar nav appears.pages/ folder. File names become navigation links automatically.st.sidebar for filters that apply to the current page.st.session_state with a widget key= to persist values across page navigation.layout="wide" in set_page_config uses the full browser width.Add a Settings page where users can toggle between light and dark chart themes. Store the theme preference in st.session_state and apply it to every Plotly chart in the app via a shared helper function.