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.
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.multiselectandst.radiofor discrete inputs;st.sliderandst.number_inputfor continuous ones. - Use
st.metricfor KPIs and rememberdelta_color="inverse"for metrics where a rise is bad news;st.dataframefor interactive tables,st.tablefor 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.