SpinnerField#
A text-entry field with up/down spin buttons for stepping through a fixed list of values or a numeric range.
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,
)
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)
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.)
|
Cross-axis placement of the widget: |
|
Claim and fill a share of the leftover vertical space (the layout
direction). |
|
External spacing in pixels. Accepts an integer (equal on all
sides), a 2-tuple |
|
Horizontal external spacing (left and right). Accepts an integer
or a 2-tuple |
|
Vertical external spacing (top and bottom). Accepts an integer
or a 2-tuple |
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.)
|
Cross-axis placement of the widget: |
|
Claim and fill a share of the leftover horizontal space (the layout
direction). |
|
External spacing in pixels. Accepts an integer (equal on all
sides), a 2-tuple |
|
Horizontal external spacing (left and right). Accepts an integer
or a 2-tuple |
|
Vertical external spacing (top and bottom). Accepts an integer
or a 2-tuple |
Grid
Used inside a Grid container.
|
Zero-based row and column indices. |
|
Number of rows or columns to span. |
|
Horizontal placement within the grid cell: |
|
Vertical placement within the grid cell: |
|
External spacing in pixels. Accepts an integer, a 2-tuple
|
|
Horizontal external spacing. Accepts an integer or |
|
Vertical external spacing. Accepts an integer or |
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:
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()