| name | using-streamlit-layouts |
| description | Structuring Streamlit app layouts. Use when placing content in sidebars, columns, containers, or dialogs. Covers sidebar usage, column limits, horizontal containers, dialogs, and bordered cards. |
| license | Apache-2.0 |
Streamlit layout
How you structure your app affects usability more than you think.
Sidebar: navigation + global filters only
The sidebar should only contain navigation and app-level filters. Main content goes in the main area.
with st.sidebar:
date_range = st.date_input("Date range")
region = st.selectbox("Region", ["All", "US", "EU", "APAC"])
st.caption("App v1.2.3")
with st.sidebar:
st.title("Dashboard")
st.dataframe(df)
st.bar_chart(data)
What goes in sidebar:
- Global filters (date range, user selection, region)
- App info (version, feedback link)
What stays out:
- Main content, charts, tables, results
Columns: max 4, set alignment
Don't use too many columns—they get cramped.
col1, col2 = st.columns(2)
col1, col2 = st.columns([2, 1])
cols = st.columns(4, vertical_alignment="center")
col1, col2, col3, col4, col5, col6 = st.columns(6)
Horizontal containers for button groups
Use st.container(horizontal=True) instead of columns for button groups:
with st.container(horizontal=True):
st.button("Cancel")
st.button("Save")
st.button("Submit")
Aligning elements
Use horizontal_alignment on containers to position elements:
with st.container(horizontal_alignment="center"):
st.image("logo.png", width=200)
st.title("Welcome")
with st.container(horizontal_alignment="right"):
st.button("Settings", icon=":material/settings:")
with st.container(horizontal=True, horizontal_alignment="distribute"):
st.button("Cancel")
st.button("Save")
st.button("Submit")
Options: "left" (default), "center", "right", "distribute"
Bordered containers
Use border=True on containers for visual grouping. See building-streamlit-dashboards for dashboard-specific patterns like KPI cards.
with st.container(border=True):
st.subheader("Section title")
st.write("Grouped content here")
Tabs
Organize content into switchable views:
tab1, tab2 = st.tabs(["Chart", "Data"])
with tab1:
st.line_chart(data)
with tab2:
st.dataframe(df)
Expander
Collapsible sections for secondary content:
with st.expander("See details"):
st.write("Hidden content here")
st.code("print('hello')")
Empty and placeholders
st.empty() creates a single-element placeholder that can be updated or cleared:
placeholder = st.empty()
placeholder.text("Loading...")
result = load_data()
placeholder.dataframe(result)
placeholder.empty()
Popover
Click to reveal content:
with st.popover("Settings"):
st.checkbox("Dark mode")
st.slider("Font size", 10, 24)
Dialogs for focused interactions
Use @st.dialog for UI that doesn't need to be always visible:
@st.dialog("Confirm deletion")
def confirm_delete(item_name):
st.write(f"Are you sure you want to delete **{item_name}**?")
if st.button("Delete", type="primary"):
delete_item(item_name)
st.rerun()
if st.button("Delete item"):
confirm_delete("My Document")
Key points:
- Dialogs rerun independently from the main script
- Use
st.session_state to pass widget values from the dialog to the main app
- Call
st.rerun() to close dialog and refresh main app
- Use
dismissible=False for forced actions
st.sidebar is not supported inside dialogs
When to use dialogs:
- Confirmation prompts
- Settings panels
- Forms that don't need to be always visible
Spacing
Control spacing between elements with gap on containers:
with st.container(gap=None, border=True):
for item in items:
st.checkbox(item.text)
with st.container(gap="small"):
...
Add vertical space with st.space:
st.space("small")
st.space("medium")
st.space("large")
st.space(50)
Width and height
Control element sizing:
cols = st.columns(2)
with cols[0].container(border=True, height="stretch"):
st.line_chart(data)
with cols[1].container(border=True, height="stretch"):
st.dataframe(df)
st.container(width="content")
st.container(height=300)
References