Skip to main content

Module 2 — Input and display components

Module 1 established the execution model. This module introduces the vocabulary Streamlit gives you to build a page: the widgets that ask the user for something, and the primitives that show a result. Everything the sales team will interact with is assembled from these calls.

Text, numbers and toggles​

Every widget follows the same pattern: it is a function that returns the current value. There is no onchange callback in the classical sense — each interaction reruns the script, so the return value is enough.

import streamlit as st

customer_id = st.text_input("Customer ID", placeholder="e.g. C-01847", max_chars=12)
tenure = st.number_input("Months as a customer", min_value=0, max_value=120, value=12, step=1)
monthly_charge = st.number_input("Monthly bill (USD)", min_value=0.0, value=70.0, step=5.0, format="%.2f")
paperless = st.toggle("Paperless billing", value=True)

Four points of habit that pay off later. placeholder= is invisible until the field is empty and does not become the value on submit, unlike a default. min_value and max_value on a number_input are enforced by the widget itself, so you do not have to re-validate. The format string follows Python's percent syntax and controls display, not storage. And st.toggle reads better than st.checkbox for a binary business setting like "paperless billing", although both return a bool.

Sliders, ranges and choices​

For continuous inputs, sliders give a better sense of scale than a text field. Streamlit supports single values, ranges and even date ranges with the same call.

tenure = st.slider("Months as a customer", 0, 72, value=12)
revenue_band = st.slider("Monthly bill range (USD)", 0.0, 200.0, value=(30.0, 90.0), step=5.0)
signup_window = st.slider(
"Signed up between",
value=(pd.Timestamp("2023-01-01"), pd.Timestamp("2024-12-31")),
)

For discrete choices, three widgets cover almost every case. st.selectbox returns one value from a list. st.multiselect returns a list of values. st.radio shows the same content as a selectbox but keeps every option visible, which is preferable when the list is short and the choice matters. st.segmented_control, added in recent versions, is a compact alternative to radio for two or three options.

contract = st.selectbox("Contract type", ["Month-to-month", "One year", "Two years"], index=0)
services = st.multiselect(
"Active services",
["Internet", "TV", "Phone", "Support+", "Cloud backup"],
default=["Internet"],
)
payment = st.radio("Payment method", ["Card", "Bank transfer", "Check"], horizontal=True)

The horizontal=True flag on st.radio lays the options side by side instead of stacked, which keeps the form compact when the labels are short.

Dates and time​

st.date_input and st.time_input return Python datetime objects, and st.date_input accepts a tuple for a range picker. Both respect the browser locale, so a user in France sees 05/09/2026 where a user in the United States sees 09/05/2026, without any code change on your side.

from datetime import date

signup_date = st.date_input("Signup date", value=date(2024, 1, 15))
review_window = st.date_input(
"Contract review window",
value=(date(2026, 1, 1), date(2026, 3, 31)),
)

Displaying text and results​

Streamlit distinguishes calls that show text (title, header, subheader, caption) from the generic st.write, which is a "do the right thing" dispatcher. Given a string it writes Markdown; given a DataFrame it renders a table; given a Matplotlib figure it draws the chart. This is convenient in a notebook, but in a real page you should prefer the explicit calls: they are faster, they document your intent, and they behave the same regardless of the argument's type.

For numeric summaries, st.metric gives a first-class KPI card with an optional delta:

col_a, col_b, col_c = st.columns(3)
col_a.metric("At-risk customers", "128", delta="+14 this month", delta_color="inverse")
col_b.metric("Recovery rate", "22 %", delta="+3 pp")
col_c.metric("Revenue at risk", "USD 42 800")

Two settings deserve attention. The delta is a string, so format it yourself; passing a raw number will render as-is without a plus sign. delta_color="inverse" swaps green and red for metrics where "up is bad" — churn count, error rate, response time. Getting this right is the difference between a dashboard the sales team trusts and one they mentally override.

For tables, st.dataframe renders an interactive Arrow-backed grid with sorting, resizing and CSV export in the top-right menu. st.table renders a static HTML table that fits a report better but does not scale beyond a few dozen rows.

The scoring form of the running example​

Here is the second version of the app, which asks the sales team every piece of information the churn model needs and displays a clean result.

import streamlit as st

st.set_page_config(page_title="Churn dashboard", page_icon="📉", layout="centered")
st.title("Churn dashboard — score a customer")

st.subheader("Contract")
c1, c2 = st.columns(2)
with c1:
customer_id = st.text_input("Customer ID", placeholder="C-01847")
tenure = st.slider("Months as a customer", 0, 72, 12)
with c2:
contract = st.selectbox("Contract type", ["Month-to-month", "One year", "Two years"])
payment = st.radio("Payment method", ["Card", "Bank transfer", "Check"], horizontal=True)

st.subheader("Billing")
c3, c4 = st.columns(2)
with c3:
monthly_charge = st.number_input("Monthly bill (USD)", 0.0, 500.0, 70.0, 5.0)
paperless = st.toggle("Paperless billing", value=True)
with c4:
services = st.multiselect(
"Active services",
["Internet", "TV", "Phone", "Support+", "Cloud backup"],
default=["Internet"],
)

if st.button("Score this customer", type="primary"):
# The real model call arrives in module 8; a placeholder for now.
score = 0.5 - 0.005 * tenure + 0.002 * monthly_charge
score += {"Month-to-month": 0.15, "One year": 0.0, "Two years": -0.10}[contract]
score = max(0.0, min(1.0, score))
st.divider()
left, right = st.columns([1, 2])
left.metric("Churn probability", f"{score:.0%}", delta_color="inverse")
right.progress(score, text="Higher is riskier.")
st.caption(f"Scored customer {customer_id or 'unknown'} with {len(services)} active services.")

The type="primary" flag turns the button into the theme's accent color so the user can see it from across the room; it is a small detail that signals what the "next step" is on a busy page. st.divider inserts a thin line between the form and the result, and st.caption writes small grey text, both of which help scannability without adding a heading.

Do not gate a heavy call on a button alone

Because the whole script reruns, code outside the if st.button block also runs on every interaction. Loading a 200 MB model in module scope will therefore make every keystroke lag — the button will not save you. The right pattern is @st.cache_resource (module 5), and only then a button to trigger the prediction.

In summary​

  • Widgets are functions that return the current value; there is no callback because the whole script re-runs on every interaction.
  • Prefer st.selectbox, st.multiselect and st.radio for discrete inputs; st.slider and st.number_input for continuous ones.
  • Use st.metric for KPIs and remember delta_color="inverse" for metrics where a rise is bad news; st.dataframe for interactive tables, st.table for a printable report.
  • The scoring form of the running example is complete; module 3 turns it into a real layout with a sidebar and tabs.

Next module: columns, tabs and sidebar, the three layout primitives that carry the dashboard from a scrolling page to a genuine tool.