SpinnerField#

A text-entry field with up/down spin buttons for stepping through a fixed list of values or a numeric range.

SpinnerField — light theme SpinnerField — dark theme

Usage#

A spinner steps with the up/down buttons or the arrow keys, in one of two modes: pass options= to cycle a list of strings, or min_value / max_value / step for a numeric range — not both. field.value is the current string (text mode) or number (numeric mode), and textsignal= binds the displayed text. For free numeric entry without a fixed step reach for NumberField; for a long list, Select.

Text mode#

Pass options= to step through a fixed list of strings. The spin buttons cycle forward and back through the list.

bs.SpinnerField(
    label="Text mode",
    options=["Small", "Medium", "Large", "X-Large"],
    value="Medium",
)

Numeric mode#

Use min_value=, max_value=, and step= instead of options= for a numeric range. Only one mode should be used at a time.

bs.SpinnerField(
    label="Numeric mode",
    value=10,
    min_value=0,
    max_value=100,
    step=5,
)
SpinnerField modes — light theme SpinnerField modes — dark theme

Value formatting#

In numeric mode, value_format= displays the value with a locale-aware ICU pattern — the raw number is preserved internally; only the display changes. Requires localization to be enabled.

bs.SpinnerField(label="Price", value=10, min_value=0, max_value=100,
                value_format="currency")

Wrap around#

Set wrap=True to cycle back to the start when the end of the list (or range) is reached, and vice versa.

bs.SpinnerField(
    label="Month",
    options=["Jan", "Feb", "Mar", "Apr", "May", "Jun",
             "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
    value="Jan",
    wrap=True,
)

Label and message#

Use label= for a field title and message= for helper text below.

bs.SpinnerField(
    label="Font size",
    value=12,
    min_value=6,
    max_value=72,
    step=2,
    message="Applies to the selected text.",
)

States#

bs.SpinnerField(value=5, min_value=1, max_value=10, label="Normal")
bs.SpinnerField(value=5, min_value=1, max_value=10, label="Read only", read_only=True)
bs.SpinnerField(value=5, min_value=1, max_value=10, label="Disabled",  disabled=True)
SpinnerField states — light theme SpinnerField states — dark theme

Reactive binding#

Bind a Signal[str] with textsignal=. The field and signal stay in sync automatically.

size = bs.Signal("M")
bs.SpinnerField(
    label="T-shirt size",
    options=["XS", "S", "M", "L", "XL", "XXL"],
    textsignal=size,
)
bs.Label(textsignal=size, accent="secondary")

Handling changes#

Use on_change() to respond when the value changes — whether from a spin button click, keyboard arrow key, or direct text entry.

sf = bs.SpinnerField(
    label="Rating",
    options=["★", "★★", "★★★", "★★★★", "★★★★★"],
    value="★★★",
    wrap=True,
)

def handle_change(e):
    print("Selected:", sf.value)

sf.on_change(handle_change)

# As a subscription (cancellable)
sub = sf.on_change(handle_change)
sub.cancel()

# As a Stream (composable)
sf.on_change().listen(handle_change)

Validation#

A spinner constrains its own values — the list, or the numeric range — so validation is usually just required for a mandatory choice. Attach rules with add_validation_rule(); they run against the field’s text value.

field = bs.SpinnerField(label="Size", options=["S", "M", "L"], required=True)

Validity is reactive state — field.valid is a Signal[bool] and field.error a Signal[str] ("" when valid). Bind the error to a label to surface it:

bs.Label(textsignal=field.error, accent="danger")   # shows and clears itself

Note

The full rule taxonomy and aggregating a whole form’s validity live in the Validation guide.

Programmatic stepping#

Call increment() and decrement() to step the value in code — the same as pressing the spin buttons. They work in both modes and honor the field’s bounds and wrap setting.

field = bs.SpinnerField(value=10, min_value=0, max_value=100, step=5)
field.increment()    # → 15
field.decrement(2)   # → 5

picker = bs.SpinnerField(value="Low", options=["Low", "Medium", "High"])
picker.increment()   # → "Medium"

Keyboard#

  • Up / Down — step to the next or previous value (the same as the spin buttons).

  • Mouse wheel — step while the field is focused.

Stepping stops at the first and last value, or wraps around when wrap=True. It does nothing while the field is read-only or disabled.

Widget sizing#

All widgets accept self-placement kwargs via **kwargs. The parent container determines which options apply — Column / Row parents use the layout kwargs below, grid-based parents use grid kwargs.

Column (vertical layout)

Used inside a Column, App, or any other container with a column layout. Children are arranged top-to-bottom, so horizontal aligns each child across the width and grow shares the vertical space. (vertical does not apply — the order of the children sets their top-to-bottom position.)

horizontal

Cross-axis placement of the widget: 'left', 'center', 'right', or 'stretch' to fill the available width.

grow

Claim and fill a share of the leftover vertical space (the layout direction). True or False.

margin

External spacing in pixels. Accepts an integer (equal on all sides), a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).

margin_x

Horizontal external spacing (left and right). Accepts an integer or a 2-tuple (left, right) for asymmetric spacing. Overrides the horizontal component of margin=.

margin_y

Vertical external spacing (top and bottom). Accepts an integer or a 2-tuple (top, bottom) for asymmetric spacing. Overrides the vertical component of margin=.

Row (horizontal layout)

Used inside a Row or any other container with a row layout. Children are arranged left-to-right, so vertical aligns each child across the height and grow shares the horizontal space. (horizontal does not apply — the order of the children sets their left-to-right position.)

vertical

Cross-axis placement of the widget: 'top', 'center', 'bottom', or 'stretch' to fill the available height.

grow

Claim and fill a share of the leftover horizontal space (the layout direction). True or False.

margin

External spacing in pixels. Accepts an integer (equal on all sides), a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).

margin_x

Horizontal external spacing (left and right). Accepts an integer or a 2-tuple (left, right) for asymmetric spacing. Overrides the horizontal component of margin=.

margin_y

Vertical external spacing (top and bottom). Accepts an integer or a 2-tuple (top, bottom) for asymmetric spacing. Overrides the vertical component of margin=.

Grid

Used inside a Grid container.

row / column

Zero-based row and column indices.

rowspan / columnspan

Number of rows or columns to span.

horizontal

Horizontal placement within the grid cell: 'left', 'center', 'right', or 'stretch' to fill the cell width.

vertical

Vertical placement within the grid cell: 'top', 'center', 'bottom', or 'stretch' to fill the cell height.

margin

External spacing in pixels. Accepts an integer, a 2-tuple (horizontal, vertical), or a 4-tuple (left, top, right, bottom).

margin_x

Horizontal external spacing. Accepts an integer or (left, right).

margin_y

Vertical external spacing. Accepts an integer or (top, bottom).

See also#

  • NumberField — numeric-only input with stepper buttons

  • Select — dropdown picker for longer lists

  • Validation — the full rule set and form-level validity

  • Signals — the reactive binding behind textsignal=

API#

The complete reference for SpinnerField lives on the Widgets API page. At a glance:

SpinnerField

A text-entry field with spin buttons for stepping through values.

Full Example#

 1
 2with bs.App(title="SpinnerField Demo", padding=20, gap=16) as app:
 3
 4    # Text mode — fixed list of options
 5    bs.Label("Text Mode", font="heading-sm")
 6    bs.SpinnerField(
 7        label="Priority",
 8        options=["Low", "Medium", "High", "Critical"],
 9        value="Medium",
10    )
11
12    # Numeric mode — min / max / step
13    bs.Label("Numeric Mode", font="heading-sm")
14    bs.SpinnerField(
15        label="Quantity",
16        value=1,
17        min_value=1,
18        max_value=99,
19        step=1,
20    )
21
22    # Wrap-around
23    bs.Label("Wrap Around", font="heading-sm")
24    bs.SpinnerField(
25        label="Month",
26        options=["Jan", "Feb", "Mar", "Apr", "May", "Jun",
27                 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
28        value="Jan",
29        wrap=True,
30    )
31
32    # Label and message
33    bs.Label("Label and Message", font="heading-sm")
34    bs.SpinnerField(
35        label="Font size",
36        value=12,
37        min_value=6,
38        max_value=72,
39        step=2,
40        message="Applies to the selected text.",
41    )
42
43    # Reactive binding
44    bs.Label("Reactive Binding", font="heading-sm")
45    size_sig = bs.Signal("Medium")
46    bs.SpinnerField(
47        label="T-shirt size",
48        options=["XS", "S", "M", "L", "XL", "XXL"],
49        value="M",
50        textsignal=size_sig,
51    )
52    bs.Label(textsignal=size_sig, accent="secondary")
53
54    # States
55    bs.Label("States", font="heading-sm")
56    bs.SpinnerField(value=5, min_value=1, max_value=10, label="Normal")
57    bs.SpinnerField(value=5, min_value=1, max_value=10, label="Read only", read_only=True)
58    bs.SpinnerField(value=5, min_value=1, max_value=10, label="Disabled",  disabled=True)
59
60    # Handling changes
61    bs.Label("Handling Changes", font="heading-sm")
62    last = bs.Signal("(none)")
63    sf = bs.SpinnerField(
64        label="Rating",
65        options=["★", "★★", "★★★", "★★★★", "★★★★★"],
66        value="★★★",
67        wrap=True,
68    )
69    sf.on_change(lambda e: last.set(sf.value))
70    bs.Label(textsignal=last, accent="secondary")
71
72app.run()