Skip to content

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.

from golit import slider

threshold: int = slider(0, 200, default=50, step=5, label="Min revenue")

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.

from golit import radio

plan: str = radio(["Free", "Pro", "Team"], default="Free")

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:

labels: list = multiselect(ALL_LABELS, default=[], label="Labels", display="dropdown")

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).

from golit import text

query: str = text(default="", placeholder="Search…", label="Query")

textarea

Multi-line text. Same commit triggers as text.

from golit import textarea

notes: str = textarea(default="", rows=6, placeholder="Notes…")

checkbox

A boolean checkbox. Posts an explicit true/false so an unchecked box still commits a value.

from golit import checkbox

include_tax: bool = checkbox(default=True, label="Include tax")

switch

A boolean toggle (a styled checkbox). Same semantics as checkbox, different look.

from golit import switch

compact: bool = switch("Compact view", default=False)

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:

  1. HTMX posts the new value to POST /node/{input_id}.
  2. Golit calls the widget's coerce() to type it, stores it for the session, and runs the dirty subgraph.
  3. 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.