Back to Streamlit

Streamlit layout

lib/streamlit/.agents/skills/developing-with-streamlit/references/layouts.md

1.58.1.dev202606227.8 KB
Original Source

Streamlit layout

How you structure your app affects usability more than you think.

Layout container overview

ContainerUse when
st.containerYou need a general-purpose group of elements, a bordered section, a horizontal row, custom alignment, fixed height, scrolling, or out-of-order insertion of multiple elements.
st.columnsYou need a simple proportional grid, such as two-column comparisons or up to four KPI cards.
st.sidebarYou need app-level navigation, global filters, settings, or small app metadata that should stay separate from the main content.
st.tabsYou need multiple peer views of related content, and users should switch between them without leaving the page. All tab content is computed by default; for lazy execution where only the selected tab runs, use on_change="rerun" (or a callable) and check each tab's .open property.
st.expanderYou need optional details, advanced settings, explanations, or diagnostic output that should not dominate the main view.
st.statusYou need to show progress, logs, or multi-step work in a collapsible status block that can update from running to complete or error.
st.popoverYou need compact on-demand controls, filters, or secondary actions without changing page layout.
@st.dialogYou need a focused modal flow, such as confirmation, short editing, or settings that should temporarily interrupt the main page.
st.formYou need to batch multiple widget inputs and rerun only when the user submits.
st.emptyYou need a placeholder that can be filled, replaced, or cleared later, including inserting elements out of order.
st.chat_messageYou need a message container with chat-specific styling and avatars. See chat-ui.md for chat interface patterns.
st.bottomYou need content pinned to the bottom of the main app area, commonly persistent chat input or bottom action controls.
st.spaceYou need explicit vertical or horizontal spacing inside the current layout direction.

The sidebar should only contain navigation and app-level filters. Main content goes in the main area.

python
# GOOD
with st.sidebar:
    date_range = st.date_input("Date range")
    region = st.selectbox("Region", ["All", "US", "EU", "APAC"])
    st.caption("App v1.2.3")
python
# BAD: Too much content in sidebar
with st.sidebar:
    st.title("Dashboard")
    st.dataframe(df)  # Don't put main content here
    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.

python
# GOOD
col1, col2 = st.columns(2)

# OK with alignment
cols = st.columns(4, vertical_alignment="center")

# BAD: Too many, cramped
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:

python
with st.container(horizontal=True):
    st.button("Cancel")
    st.button("Save")
    st.button("Submit")

Aligning elements

Use horizontal_alignment on containers to position elements:

python
# Center elements
with st.container(horizontal_alignment="center"):
    st.image("logo.png", width=200)
    st.title("Welcome")

# Right-align elements
with st.container(horizontal_alignment="right"):
    st.button("Settings", icon=":material/settings:")

# Distribute evenly (great for button groups)
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 dashboards.md for dashboard-specific patterns like KPI cards.

python
with st.container(border=True):
    st.subheader("Section title")
    st.write("Grouped content here")

Placeholders with st.empty

Use st.empty() when you need to reserve a slot and fill it later, replace one element with another, clear an element, or insert content out of order. The returned placeholder is a single-element container. If you need to replace a group of elements, put a child st.container() inside the placeholder.

python
dataframe_slot = st.empty()

rows_per_page = 25
num_pages = max(1, (len(df) + rows_per_page - 1) // rows_per_page)

with st.container(horizontal_alignment="right"):
    page = st.pagination(num_pages, key="results_page")

start = (page - 1) * rows_per_page
end = start + rows_per_page
dataframe_slot.dataframe(df.iloc[start:end], width="stretch")

This is useful when a control should appear below an element but the control's value is needed before that element renders, such as pagination below a dataframe. It also works for progress updates, temporary status messages, wizard-like flows, and cases where later code needs to render above content that has already been written. For persistent multi-element sections that do not need replacement, use st.container() instead.

Dialogs for focused interactions

Use @st.dialog for UI that doesn't need to be always visible:

python
@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")

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:

python
# Remove spacing for tight list-like UIs
with st.container(gap=None, border=True):
    for item in items:
        st.checkbox(item.text)

# Explicit gap sizes
with st.container(gap="small"):
    ...

Add vertical space with st.space:

python
st.space("small")   # Small gap
st.space("medium")  # Medium gap
st.space("large")   # Large gap
st.space(50)        # Custom pixels

Width and height

Control element sizing:

python
# Stretch to fill available space (equal height columns)
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)

# Shrink to content size
st.container(width="content")

# Fixed pixel sizes
st.container(height=300)

References