Inputs & widgets¶
A widget is a control the user interacts with. You use one as the default value of a node parameter, and Golit turns that parameter into an input node named after it.
@app.reactive
def filtered(data, threshold: int = slider(0, 200, default=50)):
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# the default is a widget → `threshold` is an input node
return data.filter(pl.col("revenue") > threshold)
When the user commits a new value, that input goes dirty and everything downstream re-runs. The function body receives the typed Python value — not the raw form string. Each widget knows how to coerce the posted string into the right type (an int, a datetime.date, a list, BytesIO, …).
Labels are automatic
If you don't pass label=, Golit derives one from the parameter name: min_revenue → "Min Revenue". Pass label= to override.
The catalog¶
All widgets are importable from the top-level golit package. Each has an ergonomic lowercase factory (slider, select, …) and an underlying class (Slider, Select, …) — use the factory.
slider¶
A numeric range slider. Commits on release (change); while dragging, an Alpine.js "local shield" shows the live value without touching the server.
slider(low, high, *, default=low, step=1, label=None). If the bounds, step, and default are all whole numbers, values coerce to int; otherwise float.
rangeslider¶
A dual-handle slider for selecting a numeric band — min and max in one control. Like slider it commits on release and shows live feedback while dragging. The value is a sorted (low, high) tuple.
from golit import rangeslider
band: tuple = rangeslider(0, 200, default=(40, 160), step=10, label="Revenue band")
@app.reactive
def filtered(data, band: tuple = rangeslider(0, 200, default=(0, 200), label="Band")):
lo, hi = band
return data.filter(pl.col("revenue").is_between(lo, hi))
rangeslider(low, high, *, default=(low, high), step=1, label=None). Same int/float coercion rule as slider; the pair is always returned sorted and clamped to the bounds.
number¶
A numeric input with optional bounds. By default it's a native number spinner. prefix/suffix add inline adornments and thousands=True groups the digits when the value commits — together a currency / percent field.
from golit import number
qty: int = number(0, 100, default=10, step=1, label="Quantity")
budget: int = number(0, 1_000_000, default=50_000, step=1000, prefix="$", thousands=True, label="Budget")
rate: float = number(0, 100, default=12.5, step=0.1, suffix="%", label="Rate")
number(low=None, high=None, *, default=0, step=1, label=None, prefix="", suffix="", thousands=False). Coerces to int when step and default are whole, else float — and tolerates a formatted string (separators, $/%), so the committed value is always clean. With thousands=True the field regroups on blur/Enter (never mid-keystroke, so the caret never jumps).
select¶
A dropdown of options; the value is the chosen option object (not its string form).
from golit import select
region: str = select(["All", "North", "South"], default="All", label="Region")
select(options, *, default=options[0], label=None).
combobox¶
A searchable single-select — same value semantics as select (the chosen option object), but the options open in a filterable popover. Reach for it over select when the list is long (customers, SKUs, accounts); picking an option commits it and closes the popover.
from golit import combobox
customer: str = combobox(CUSTOMERS, label="Customer", placeholder="Pick a customer…")
combobox(options, *, default=options[0], label=None, placeholder="Select…"). The value coerces back to the option object just like select.
radio¶
A single choice shown as radio buttons. Same value semantics as select.
segmented¶
A single choice as a compact horizontal toggle (a segmented control) — same value semantics as radio/select, but tuned for short sets like Day/Week/Month. The selected segment highlights white-on-blue and commits on click.
from golit import segmented
grain: str = segmented(["Daily", "Weekly", "Monthly"], default="Daily", label="Granularity")
segmented(options, *, default=options[0], label=None).
multiselect¶
Zero or more choices, as a checkbox group. The value is a list of the chosen option objects, in option order.
from golit import multiselect
regions: list = multiselect(["North", "South", "East", "West"], default=["North"], label="Regions")
multiselect(options, *, default=(), label=None, display="list").
For long option lists, pass display="dropdown" to collapse the checkboxes into a searchable popover — a trigger button that summarises the selection, opening a panel with a search box that filters the options as you type:
tags¶
A free-form token input — the user types arbitrary values and each becomes a removable chip. Unlike multiselect the values aren't from a fixed list (recipients, keywords, ad-hoc filter terms). The value is a list[str].
from golit import tags
keywords: list = tags(default=["urgent"], label="Keywords", placeholder="Add a keyword…")
tags(*, default=(), label=None, placeholder="Add tag…"). Enter or comma commits the draft as a chip, Backspace on an empty field removes the last chip, and × removes any. Because the chips are comma-joined into the one value, a chip can't itself contain a comma.
text¶
A single-line text input. Commits on blur (change) and after a short typing pause (keyup debounced ~400ms).
textarea¶
Multi-line text. Same commit triggers as text.
checkbox¶
A boolean checkbox. Posts an explicit true/false so an unchecked box still commits a value.
switch¶
A boolean toggle (a styled checkbox). Same semantics as checkbox, different look.
date¶
A native date picker. Coerces the ISO string to a datetime.date (or None when empty).
import datetime
from golit import date
day: datetime.date = date(default=datetime.date(2026, 1, 1), label="As of")
daterange¶
A start/end period picker — the filter most dashboards lead with. Two native date inputs plus quick presets (last 7/30/90 days, MTD/QTD/YTD) computed in the browser against today. The value is a (start, end) tuple of datetime.date, where either side may be None when left blank.
import datetime
from golit import daterange
period: tuple = daterange(
default=(datetime.date(2026, 4, 1), datetime.date(2026, 6, 30)),
low=datetime.date(2026, 1, 1), # optional bounds for the pickers
label="Period",
)
daterange(*, default=None, label=None, low=None, high=None, presets=True). Both edges post comma-joined into the one value, so unpack the tuple in your node and guard each side:
@app.reactive
def filtered(data, period: tuple = daterange(label="Period")):
lo, hi = period
if lo is not None:
data = data.filter(pl.col("date") >= lo)
if hi is not None:
data = data.filter(pl.col("date") <= hi)
return data
Pass presets=False to hide the quick chips.
upload¶
A file upload. Coerces the posted bytes into a BytesIO, which Polars readers accept directly — so you can pass it straight to pl.read_csv.
import polars as pl
from golit import upload
@app.source
def data(file=upload("Upload CSV", accept=".csv")) -> pl.DataFrame:
return SAMPLE if file is None else pl.read_csv(file)
upload(label=None, *, accept=None). The value is None until a file is chosen, so guard for it.
button¶
An action trigger — the reactive equivalent of "on click". Each click posts a fresh nonce (a monotonic counter), so the input's value changes and its dirty subgraph re-runs. The value itself is usually ignored.
from golit import button
@app.view
def report(go: int = button("Generate", kind="primary")) -> str:
# Re-runs on every click because `go` changes each time.
return build_report()
button(label=None, *, kind="primary") — kind is "primary", "secondary", or "ghost".
How a value travels¶
When you move a control:
- HTMX posts the new value to
POST /node/{input_id}. - Golit calls the widget's
coerce()to type it, stores it for the session, and runs the dirty subgraph. - The response carries only the changed view fragments, swapped in place.
High-frequency events (slider drag, keystrokes) are absorbed client-side by Alpine and HTMX's debouncing, so the server only sees committed changes — see Architecture: the Local Shield.
Multiple inputs, one node¶
A node can take any number of inputs and dependencies together — they're just parameters:
@app.reactive
def filtered(
data: pl.DataFrame, # dependency
threshold: int = slider(0, 200, default=50), # input
region: str = select(REGIONS, default="All"), # input
) -> pl.DataFrame:
out = data.filter(pl.col("revenue") > threshold)
return out if region == "All" else out.filter(pl.col("region") == region)
Changing either input dirties filtered and everything downstream of it — and nothing else.
Next¶
Now the other half of a node function: what a view can return. See Views & rendering.