What Is a Streamlit Button? How to Use st.button
Updated on
A Streamlit button is the simplest way to let a user trigger an action in your app.
What a Streamlit button means
When people talk about a "button" in Streamlit, they usually mean a widget that says, "run this action now."
That action might be:
- refreshing a dataset
- revealing more controls
- moving a workflow to the next step
- kicking off a calculation
- confirming a user choice
In Streamlit, widgets cause the script to rerun from top to bottom. A button is not a persistent on/off control. It is a momentary trigger that becomes True on the rerun immediately after the click.
Why Streamlit buttons matter
Buttons matter because many apps need a clear point where the user says, "do it now."
That is different from widgets like st.selectbox or sliders, which continuously hold a current value. A button is better when you want a deliberate action instead of a persistent state.
In real projects, that usually means:
- running a search only after the user finishes setting filters
- confirming a form step
- resetting app state
- exporting or processing data after an upload
What st.button is actually for
If you are deciding whether this widget is worth using, the practical answer is simple: st.button is for actions, not settings.
Use it when you want to:
- trigger logic once
- coordinate a step in a workflow
- update
st.session_state - pair an action with another widget such as
st.file_uploader
Do not use it when you really need a persistent control. In those cases, widgets such as st.checkbox, st.toggle, or st.radio are usually clearer.
After reading this guide, you should be able to:
- understand why buttons can feel "stateless"
- trigger logic without messy rerun bugs
- choose between inline
ifblocks and callbacks - build cleaner button-driven flows in real apps
Quick Start
This first example shows the basic mental model: a button returns True only for the rerun triggered by the click.
import streamlit as st
if st.button("Say hello"):
st.success("Hello, Streamlit!")That small behavior explains most of the confusion around Streamlit buttons. The button is not "stuck on." It simply reports that it was clicked on the current rerun.
Why buttons sometimes confuse new Streamlit users
If you come from front-end frameworks or desktop UI toolkits, you may expect the button itself to remember that it was pressed.
That is not how Streamlit works.
The important mental model is:
- the user clicks the button
- Streamlit reruns the script
st.button(...)returnsTruefor that rerun- later reruns return
Falseagain unless the user clicks again
If you want the app to remember what happened after the click, store that result in st.session_state.
Why Streamlit buttons feel different from traditional buttons
This is the deeper mindset shift that experienced readers usually care about.
Streamlit is mainly a top-to-bottom dataflow framework, while a button is an event-like widget inside that flow.
That creates a behavior that feels unusual at first:
- the click does not permanently flip the button to an "on" state
- the click triggers a rerun of the whole script
- code below the button can react on that rerun
- code above the button has already executed for that rerun unless you stored state and reran again
That is why people often ask questions such as:
- is the button
Trueforever after one click? - does the click affect code above the button?
- when does the button reset back to
False?
The practical answer is:
st.button(...)isTrueonly on the rerun caused by that click- on later reruns it becomes
Falseagain unless the user clicks again - if you need lasting effects, write them into
st.session_state
The order of execution matters
The easiest mental model is:
- the user clicks the button
- Streamlit updates widget state
- an optional callback runs first
- the script reruns from top to bottom
st.button(...)isTrueonly on that rerun
This is why a callback can sometimes feel cleaner than inline button logic. It lets you update durable state first, then render the page from that state.
Why code below reacts more naturally than code above
If your button sits in the middle of the script, code after the button sees the current rerun result immediately.
Code before the button has already executed by the time Streamlit reaches st.button(...).
That is also why some button-driven flows use st.session_state plus st.rerun() when the result should reshape content earlier on the page.
Core arguments you should know
| Argument | What it does | When to care |
|---|---|---|
label | Sets the button text | Always |
key | Gives the widget a stable identity | Needed when labels repeat |
help | Adds a tooltip | Useful for secondary actions |
on_click | Runs a callback after the click | Good for cleaner logic |
args / kwargs | Passes values into the callback | Useful when reusing handlers |
type | Chooses primary, secondary, or tertiary styling | Helpful for visual emphasis |
icon | Adds an icon to the button | Useful for scanability |
disabled | Prevents interaction | Good for gated actions |
width | Controls the rendered width | Use width="stretch" for full-width buttons |
use_container_width | Legacy width option | Deprecated in favor of width |
A modern st.button example
This example shows the current API style, including type, icon, and width.
import streamlit as st
def refresh_data(limit: int) -> None:
st.session_state.last_refresh_limit = limit
st.session_state.last_refresh_status = "done"
st.session_state.setdefault("last_refresh_limit", None)
st.session_state.setdefault("last_refresh_status", "idle")
st.button(
"Refresh data",
type="primary",
icon=":material/refresh:",
width="stretch",
on_click=refresh_data,
kwargs={"limit": 500},
)
st.write("Status:", st.session_state.last_refresh_status)
st.write("Last limit:", st.session_state.last_refresh_limit)This pattern is usually easier to maintain than burying all the logic inside a large if st.button(...) block.
Use callbacks when the click should update app state
Callbacks are the cleanest option when one click should update several values, especially in apps with multiple steps or derived UI.
This example shows a button saving a timestamp into session state.
import datetime as dt
import streamlit as st
st.session_state.setdefault("last_clicked", None)
def mark_timestamp() -> None:
st.session_state.last_clicked = dt.datetime.now(dt.timezone.utc)
st.button("Record timestamp", on_click=mark_timestamp, type="primary")
st.write("Last click:", st.session_state.last_clicked or "Not yet")If the click should change what the user sees later, callbacks plus session state are usually the right combination.
Common button patterns in real apps
Reveal more controls
This pattern is useful when you want to keep the page simple until the user asks for advanced options.
import streamlit as st
st.session_state.setdefault("show_advanced", False)
if st.button("Toggle advanced options"):
st.session_state.show_advanced = not st.session_state.show_advanced
if st.session_state.show_advanced:
st.selectbox("Model", ["small", "base", "large"])
st.checkbox("Enable debug mode")Trigger processing after an upload
Buttons work well when a file upload should not immediately launch expensive processing.
import pandas as pd
import streamlit as st
uploaded_file = st.file_uploader("Upload a CSV", type=["csv"])
if uploaded_file is not None and st.button("Analyze file", type="primary"):
df = pd.read_csv(uploaded_file)
st.dataframe(df.head())This is often a better experience than processing the file on every small widget change.
Build action rows with columns
Buttons are often easier to scan when they are grouped inside st.columns.
import streamlit as st
col1, col2 = st.columns(2)
with col1:
st.button("Back", width="stretch")
with col2:
st.button("Next", type="primary", width="stretch")For action-heavy layouts, columns make the page feel more intentional and easier to navigate.
When not to use st.button
Sometimes the widget is technically valid but still the wrong choice.
Use another widget when:
- the user should keep a value selected: use
st.selectbox,st.radio, orst.checkbox - the action is a file download: use
st.download_button - the action is tied to a form submission: use
st.form_submit_button - the app needs a persistent on/off state: use
st.toggleorst.checkbox
Choosing the right widget usually makes the app easier to understand before you write any extra logic.
Styling and layout notes
Most button styling should come from Streamlit's built-in options rather than custom CSS.
In practice, this is usually enough:
type="primary"for the main actiontype="secondary"for standard actionstype="tertiary"for lower-emphasis actionsicon=...for faster scanningwidth="stretch"for full-width layouts
If you still find yourself fighting the layout, the problem is often structural rather than visual. Reorganizing the page with st.columns, st.container, or st.sidebar is usually better than overriding internal CSS.
Troubleshooting
Why does my button stop being True right away?
Because st.button only reports the click on the rerun immediately after the click. If you need persistence, store the result in st.session_state.
Why does my button logic rerun unexpectedly?
Because the whole script reruns whenever widgets change. Keep button-triggered work inside a clear branch or callback, and cache expensive data work with st.cache_data when appropriate.
Why am I seeing a use_container_width warning?
Newer Streamlit versions prefer the width parameter. Replace use_container_width=True with width="stretch" for the same layout intent.
Why does my callback feel easier to manage than if st.button(...)?
Because callbacks separate "what happened" from "how the page is rendered." That tends to scale better as the app grows.
Related Guides
- Streamlit Session State
- Streamlit Selectbox
- Streamlit Columns
- Streamlit Upload File
- Streamlit Caching
Frequently Asked Questions
What does st.button do in simple terms?
It shows a clickable button and returns True on the rerun immediately after the user clicks it.
When should I use a button in Streamlit?
Use it when the user needs to trigger an action such as refreshing data, moving to the next step, or confirming a choice.
How do I make a full-width button in Streamlit?
Use width="stretch" on st.button.
What replaced use_container_width for Streamlit buttons?
Use the width parameter instead. In most cases, width="stretch" is the modern replacement.
How do I make a button remember something after the click?
Save the result in st.session_state or update state inside an on_click callback.