Skip to main content

Module 3 — Layout: columns, tabs, sidebar

Modules 1 and 2 built widgets that stacked from top to bottom. That works for a proof of concept, but no sales team wants to scroll through a page of thirty inputs before seeing a chart. This module introduces the four layout primitives Streamlit gives you — columns, tabs, the sidebar and expanders — and combines them into the dashboard layout the running example will keep from now on.

Containers: the mental model

Every layout primitive returns a container, and containers behave like miniature scripts: whatever you call inside them appears inside them, whatever you call outside stays outside. Two syntaxes coexist and both are common in real code.

import streamlit as st

# Syntax A: the with-block. Clearer when the block is long.
with st.container():
st.subheader("Contract")
st.text_input("Customer ID")

# Syntax B: attribute access on the returned handle. Compact for a one-liner.
c = st.container(border=True)
c.metric("At-risk customers", "128")

The border=True flag added in recent versions draws a subtle outline around the container, which is often enough to visually group a KPI without a heading. st.container(height=300, border=True) even gives you a scrollable area of a fixed pixel height — useful for a live log without letting it push the rest of the page below the fold.

Columns

st.columns splits the current container horizontally. It accepts either a count or a list of weights.

left, right = st.columns(2)                              # two equal columns
narrow, wide = st.columns([1, 3]) # 25 % / 75 %
a, b, c = st.columns(3, gap="large", vertical_alignment="center")

left.metric("Recovery rate", "22 %")
with right:
st.line_chart(monthly_revenue) # placeholder chart

Three details save time when the design starts to grow. Weights are relative, so [1, 3] and [2, 6] produce identical widths. The gap argument accepts "small", "medium" or "large" and is applied between every pair of columns, not once for the row. And vertical_alignment="center" on a row of columns aligns their contents vertically, which is what you want when a metric sits next to a paragraph of text — otherwise the metric sticks to the top.

Tabs

st.tabs is the natural way to give one page several views without scrolling. The user pays a click, and you keep the URL clean.

tab_score, tab_pipeline, tab_history = st.tabs(["Score a customer", "Batch pipeline", "History"])

with tab_score:
st.text_input("Customer ID", key="single_customer")
# ... form from module 2 ...

with tab_pipeline:
st.file_uploader("Upload CSV", type=["csv"], key="batch_csv")

with tab_history:
st.dataframe(recent_predictions) # placeholder

Two facts worth internalizing. Streamlit runs the code inside every tab on every rerun, even the tabs the user is not looking at. If a tab contains an expensive call, hide it behind a cache (module 5) or a button, not behind the tab. Also, st.tabs returns the tab handles in the order of their labels, so pinning the same order in a list makes the code robust to renaming.

The sidebar

st.sidebar is the ideal home for filters and settings that apply to every view of the app: date range, segment, region, whether to hide retained customers. Anything the user should be able to change without losing their current view goes there.

with st.sidebar:
st.title("Filters")
date_range = st.date_input("Signup between", (date(2023, 1, 1), date(2026, 12, 31)))
segment = st.multiselect("Segment", ["SMB", "Mid-market", "Enterprise"], default=["SMB", "Mid-market"])
threshold = st.slider("At-risk threshold", 0.0, 1.0, 0.60, 0.05)
st.divider()
st.caption("Filters apply to every tab.")

The sidebar can be collapsed by the user, so never put anything the app needs to function there — a required setting hidden behind a folded sidebar is a real support ticket. It can also be pinned open by passing initial_sidebar_state="expanded" to st.set_page_config. On mobile, the sidebar becomes a drawer that opens on tap, so keep it short: three or four filters at most, or the drawer becomes its own scrolling page.

Expanders and popovers

st.expander folds a section behind a title. Use it for details a user occasionally wants to see — the raw JSON payload, the model's per-feature contribution, a debug panel:

with st.expander("Show the model inputs"):
st.json(payload)

st.popover is a newer, lighter alternative: it opens on click as a floating panel, without pushing content down the page. It fits secondary actions and short forms — "add a note", "rename the customer" — better than an expander.

with st.popover("Add a note"):
note = st.text_area("Free-form note")
st.button("Save")

The dashboard layout of the running example

Here is the third version of the app. The single scrolling page becomes a real dashboard: filters in the sidebar, three tabs for the three ways the sales team uses the tool, columns to put KPIs and a form side by side.

import streamlit as st
import pandas as pd
from datetime import date

st.set_page_config(page_title="Churn dashboard", page_icon="📉", layout="wide",
initial_sidebar_state="expanded")

with st.sidebar:
st.title("Filters")
segment = st.multiselect("Segment", ["SMB", "Mid-market", "Enterprise"],
default=["SMB", "Mid-market"])
threshold = st.slider("At-risk threshold", 0.0, 1.0, 0.60, 0.05)
st.caption("Applies to every tab.")

st.title("Churn dashboard")

k1, k2, k3, k4 = st.columns(4, gap="large")
k1.metric("At-risk customers", "128", delta="+14 vs last month", delta_color="inverse")
k2.metric("Revenue at risk", "USD 42 800")
k3.metric("Recovery rate", "22 %", delta="+3 pp")
k4.metric("Model version", "v3.2")

st.divider()

tab_score, tab_batch, tab_history = st.tabs(
["Score a customer", "Batch pipeline", "History"]
)

with tab_score:
left, right = st.columns([1, 1], gap="large")
with left:
# The scoring form from module 2 lives here.
st.text_input("Customer ID", placeholder="C-01847")
st.slider("Months as a customer", 0, 72, 12)
st.selectbox("Contract type", ["Month-to-month", "One year", "Two years"])
st.button("Score this customer", type="primary")
with right:
st.info("The result appears here after you click **Score this customer**.")
with st.expander("Advanced options"):
st.checkbox("Show per-feature contribution")

with tab_batch:
st.caption("Batch scoring lives in module 7.")

with tab_history:
st.caption("The prediction history table lives in module 4.")

The layout="wide" in set_page_config uses the full browser width rather than a centered 730 px column: a dashboard with four KPI cards needs the space. And the top-of-file set_page_config is still the only place that call is legal — moving it below the sidebar block will crash the app.

Right-to-left languages

For an Arabic or Hebrew version of your dashboard, Streamlit's layout primitives work unchanged, but you need direction: rtl on the root of the page. The .streamlit/config.toml file introduced in module 9 lets you inject CSS site-wide, and the sidebar swaps to the right of the screen by that single setting.

In summary

  • Every layout primitive is a container; use with for long blocks and attribute access for one-liners.
  • Columns split space horizontally, tabs split it by view, expanders and popovers fold optional details; use them together.
  • The sidebar holds filters and settings that cross every view; never put a required setting there — the user can collapse it.
  • Code inside a hidden tab still runs on rerun; put expensive work behind a cache (module 5), not behind a tab.

Next module: charts and tables, the two visual pillars any dashboard — including the running example — is built on.