Skip to content

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:

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 if blocks 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:

  1. the user clicks the button
  2. Streamlit reruns the script
  3. st.button(...) returns True for that rerun
  4. later reruns return False again 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 True forever 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(...) is True only on the rerun caused by that click
  • on later reruns it becomes False again 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:

  1. the user clicks the button
  2. Streamlit updates widget state
  3. an optional callback runs first
  4. the script reruns from top to bottom
  5. st.button(...) is True only 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

ArgumentWhat it doesWhen to care
labelSets the button textAlways
keyGives the widget a stable identityNeeded when labels repeat
helpAdds a tooltipUseful for secondary actions
on_clickRuns a callback after the clickGood for cleaner logic
args / kwargsPasses values into the callbackUseful when reusing handlers
typeChooses primary, secondary, or tertiary stylingHelpful for visual emphasis
iconAdds an icon to the buttonUseful for scanability
disabledPrevents interactionGood for gated actions
widthControls the rendered widthUse width="stretch" for full-width buttons
use_container_widthLegacy width optionDeprecated 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, or st.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.toggle or st.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 action
  • type="secondary" for standard actions
  • type="tertiary" for lower-emphasis actions
  • icon=... for faster scanning
  • width="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

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.