TextField#

Single-line text input with optional label, helper text, placeholder, validation, and reactive signal binding.

TextField demo — light theme TextField demo — dark theme

Usage#

A field keeps three things distinct: the label beside it, the value you read and write in code, and the display text the user sees. Read the current input with field.value, or bind a Signal with textsignal= to keep a variable and the field in lockstep. Input events fire in two beats — on_input on every keystroke, on_change once the value is committed (on blur or Enter) — so you choose live feedback or settled value per handler.

Basic#

bs.TextField(placeholder="Type something…")

Label and message#

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

bs.TextField(
    label="Email address",
    placeholder="you@example.com",
    message="We'll never share your email.",
)

Required#

Set required=True to mark the field visually and prevent empty submission.

bs.TextField(label="Username",     required=True, placeholder="Required field")
bs.TextField(label="Email address", placeholder="Optional field")
TextField required — light theme TextField required — dark theme

Value formatting#

Use value_format= to display the value with a locale-aware ICU pattern. The raw string value is preserved internally; only the display changes. Requires localization to be enabled.

bs.TextField(value="1234.5",     value_format="#,##0.00",  label="Decimal")
bs.TextField(value="0.42",       value_format="percent",   label="Percent")
bs.TextField(value="9.99",       value_format="currency",  label="Currency")
bs.TextField(value="2024-06-01", value_format="yyyy-MM-dd", label="Date")
TextField value formatting — light theme TextField value formatting — dark theme

States#

bs.TextField(value="Editable",  label="Normal")
bs.TextField(value="Read only", label="Read only", read_only=True)
bs.TextField(value="Disabled",  label="Disabled",  disabled=True)
TextField states — light theme TextField states — dark theme

Reactive binding#

Bind a Signal[str] with textsignal=. The field and signal stay in sync automatically — typing updates the signal, setting the signal updates the field.

name = bs.Signal("World")
bs.TextField(label="Name", textsignal=name)
bs.Label(textsignal=name, accent="secondary")   # updates as you type

Live input events#

on_input() fires on every keystroke. Use it for real-time feedback, character counting, or live search — anything that needs to respond to typing, not just field exit.

count = bs.Label("0 / 100", accent="secondary", font="caption")
field = bs.TextField(placeholder="Type…")

def _update_count(e):
    count.text = f"{len(field.value)} / 100"

field.on_input(_update_count)

# Or as a debounced Stream
field.on_input().debounce(300).listen(lambda e: search(field.value))

Committed changes#

on_change() fires once the value is committed — when the field loses focus or the user presses Enter — not on every keystroke. Reach for it when the work is expensive or should run on the settled value (saving, recomputing a total). The handler receives a ChangeEvent with the parsed value and the previous one.

bs.TextField(label="Display name").on_change(lambda e: save(e.value))

Submit on Enter#

field = bs.TextField(placeholder="Search…")
field.on_submit(lambda e: run_search(field.value))

Validation#

Attach rules with add_validation_rule(). Rules run on the configured trigger ('blur', 'key', or 'manual'), and validate the field’s typed value — for a plain text field that is the string itself.

field = bs.TextField(label="Username")
field.add_validation_rule(
    "stringLength",
    message="Must be at least 3 characters.",
    min=3,
    trigger="blur",
)

is_valid = field.validate()   # run every rule on demand
TextField validation — light theme TextField validation — dark theme

Validity is reactive state. field.valid is a Signal[bool] and field.error a Signal[str] (the current message, "" when valid) — bind the error straight to a label and it keeps itself in sync, or drive a submit button off field.valid:

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

Note

This is the field-level slice. The rule taxonomy (text rules vs value rules), the range rule, compare and custom rules, how the typed value is resolved, and aggregating a whole form’s validity all live in the Validation guide.

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#

  • Validation — the full rule set, typed-value model, and form-level validity.

  • Customizing Fields — add buttons or icons inside a field, and subclass it into a reusable type.

  • PasswordField — a text field with a built-in mask and reveal toggle.

  • NumberField, DateField — typed siblings for numeric and date input.

  • Signals — the reactive binding behind textsignal=.

API#

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

TextField

Single-line text input with optional label, message, and validation.

Full Example#

 1
 2with bs.App(title="TextField Demo", padding=20, gap=16) as app:
 3
 4    # Basic
 5    bs.Label("Basic", font="heading-sm")
 6    basic = bs.TextField(value="Hello, bootstack!", horizontal="stretch")
 7
 8    # Label and message
 9    bs.Label("Label and Message", font="heading-sm")
10    bs.TextField(
11        label="Email address",
12        placeholder="you@example.com",
13        message="We'll never share your email.",
14        horizontal="stretch",
15    )
16
17    # Required
18    bs.Label("Required", font="heading-sm")
19    bs.TextField(label="Username", placeholder="Choose a username", required=True, horizontal="stretch")
20
21    # States
22    bs.Label("States", font="heading-sm")
23    with bs.Row(gap=8, horizontal="stretch", grow_items=True):
24        bs.TextField(value="Editable",  label="Normal")
25        bs.TextField(value="Read only", label="Read only", read_only=True)
26        bs.TextField(value="Disabled",  label="Disabled", disabled=True)
27
28    # Reactive binding
29    bs.Label("Reactive Binding", font="heading-sm")
30    with bs.Column(gap=6, horizontal="stretch"):
31        name = bs.Signal("bootstack")
32        bs.TextField(label="Name", textsignal=name, horizontal="stretch")
33        bs.Label(textsignal=name, accent="secondary", font="caption")
34
35    # Live character count via on_input
36    bs.Label("Live Character Count (on_input)", font="heading-sm")
37    with bs.Column(gap=4, horizontal="stretch"):
38        count_lbl = bs.Label("0 / 50", accent="secondary", font="caption")
39        field = bs.TextField(placeholder="Type to count…", horizontal="stretch")
40
41        def _update_count(e):
42            count_lbl.text = f"{len(field.value)} / 50"
43
44        field.on_input(_update_count)
45
46    # on_submit
47    bs.Label("Submit on Enter (on_submit)", font="heading-sm")
48    with bs.Column(gap=4, horizontal="stretch"):
49        result_lbl = bs.Label("", accent="success", font="caption")
50        submit_field = bs.TextField(placeholder="Type and press Enter…", horizontal="stretch")
51        def _on_submit(e):
52            result_lbl.text = f"Submitted: {submit_field.value}"
53
54        submit_field.on_submit(_on_submit)
55
56    # Validation
57    bs.Label("Validation", font="heading-sm")
58    vf = bs.TextField(label="Min 3 characters", placeholder="Enter text…",
59                      required=True, horizontal="stretch")
60    vf.add_validation_rule(
61        "stringLength",
62        message="Must be at least 3 characters.",
63        min=3,
64        trigger="blur",
65    )
66
67app.run()