Form#

bs.Form builds a data-entry layout from a dict or an explicit list of field definitions. Fields are placed on a grid; GroupItem creates labeled sections and TabsItem creates a tabbed layout.

Form demo — light theme Form demo — dark theme

Usage#

A Form turns a dict (or a list of field definitions) into a laid-out, validating data-entry surface — fields map to the right widget by value type, form.values reads them all back, and form.valid tracks validity reactively. Embed it in a page, or use a FormDialog for a modal.

Auto-generated fields#

Pass a dict to data=. Keys become field labels; value types determine the editor widget automatically:

bs.Form(
    data={
        "name": "Alice Smith",     # str  → text entry
        "email": "alice@example.com",
        "age": 30,                 # int  → numeric entry
        "active": True,            # bool → checkbox
    },
)

The returned value dict has the same keys as data, filled with the user’s current input.

Multiple columns#

Use col_count= to distribute fields across multiple columns:

bs.Form(
    data={
        "street": "123 Main St",
        "city": "Springfield",
        "state": "IL",
        "zip": "62701",
    },
    col_count=2,
)
Form multiple columns — light theme Form multiple columns — dark theme

Explicit fields#

Use FieldItem for full control over each field — label, type hint, editor, and grid placement:

bs.Form(
    items=[
        bs.FieldItem(key="username", label="Username"),
        bs.FieldItem(key="password", label="Password", dtype="password"),
        bs.FieldItem(key="role", label="Role",
                     editor="select",
                     editor_options={"values": ["Admin", "Editor", "Viewer"]}),
    ],
)

Editor types#

The editor= argument on FieldItem forces a specific widget regardless of the field’s value type. Editor names match the corresponding bs.* widget class name in lowercase:

Editor

Public widget

Notes

'textfield'

TextField

Single-line text input. Default for str.

'numberfield'

NumberField

Numeric input with stepper buttons. Default for int / float.

'passwordfield'

PasswordField

Masked text input. Default for dtype='password'.

'datefield'

DateField

Date picker with calendar popup. Default for date / datetime.

'textarea'

TextArea

Multi-line text editor.

'select'

Select

Drop-down list. Requires editor_options={"values": [...]}. Pass editor_options={"allow_custom_values": True} for an editable combobox.

'spinnerfield'

SpinnerField

Numeric spinner field.

'checkbox'

Checkbox

Checkbox control. Default for bool.

'switch'

Switch

Toggle switch.

'slider'

Slider

Horizontal slider.

Editor options#

editor_options are the keyword arguments of the editor’s public widget — the same names you would pass when constructing that bs.* widget directly. For a numberfield that means NumberField options such as step and min_value; for a textarea it means TextArea options such as show_border:

bs.Form(
    items=[
        bs.FieldItem(key="quantity", label="Quantity", editor="numberfield",
                     editor_options={"step": 10, "min_value": 0}),
        bs.FieldItem(key="notes", label="Notes", editor="textarea",
                     editor_options={"height": 5, "show_border": True}),
    ],
)

An option that names something the form also fills — such as label, or a select’s choices — overrides the form’s own default. Two behave differently: value seeds the editor only when the form’s data carries nothing for that key, so your record always wins; and parent is owned by the form, which places every editor in its own field container.

Grouped fields#

Use GroupItem to create a labeled section with its own column layout:

bs.Form(
    items=[
        bs.GroupItem(
            label="Contact",
            col_count=2,
            items=[
                bs.FieldItem(key="first_name", label="First Name"),
                bs.FieldItem(key="last_name",  label="Last Name"),
                bs.FieldItem(key="email",      label="Email"),
                bs.FieldItem(key="phone",      label="Phone"),
            ],
        ),
    ],
)
Form grouped fields — light theme Form grouped fields — dark theme

Groups can be nested and placed in specific grid cells using column=, row=, and columnspan=.

Tabbed layouts#

Use TabsItem with TabItem to organize fields into tabs:

bs.Form(
    items=[
        bs.TabsItem(tabs=[
            bs.TabItem(
                label="Account",
                items=[
                    bs.FieldItem(key="username", label="Username"),
                    bs.FieldItem(key="password", label="Password", dtype="password"),
                ],
            ),
            bs.TabItem(
                label="Profile",
                items=[
                    bs.FieldItem(key="bio",     label="Bio",     editor="textarea"),
                    bs.FieldItem(key="website", label="Website"),
                ],
            ),
        ]),
    ],
)
Form tabbed layout — light theme Form tabbed layout — dark theme

Validation#

Call validate() to run validation rules on all fields. Access individual field widgets via field(key) to attach rules:

form = bs.Form(data={"email": "", "username": ""})

form.field("email").add_validation_rule(
    "email", message="Enter a valid email address.", trigger="blur"
)
form.field("username").add_validation_rule(
    "stringLength", min=3, message="At least 3 characters.", trigger="blur"
)

if form.validate():
    submit(form.value)

Reactive updates#

Use on_data_change= at construction time, or call on_data_change() after construction. Both receive the current form data as a dict:

# constructor kwarg
bs.Form(
    data={"title": "", "description": ""},
    on_data_change=lambda data: preview(data),
)

# event shorthand — returns a Subscription
form = bs.Form(data={"title": "", "description": ""})
form.on_data_change(lambda data: preview(data))

# composable Stream
form.on_data_change().debounce(300).listen(lambda data: preview(data))

Reading and writing values#

form = bs.Form(data={"name": "Alice", "age": 30})

# Read all values
values = form.value        # {'name': 'Alice', 'age': 30}
values = form.get()        # equivalent

# Write values — only the keys you pass are changed
form.value = {"name": "Bob", "age": 25}
form.set({"name": "Bob", "age": 25})   # equivalent
form.set({"age": 26})                  # writes age; name is left alone

# Read / write a single field
name = form.get_field_value("name")
form.set_field_value("name", "Carol")

# Reactive access
sig = form.field_signal("age")
sig.subscribe(lambda v: print(f"age changed: {v}"))

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#

Form DialogFormDialog embeds a Form in a dialog window with built-in OK / Cancel buttons.

API#

The complete reference for Form and its item types — FieldItem, GroupItem, TabsItem, TabItem, and the FormItem union — lives on the Widgets API page. At a glance:

Form

Data-entry form built from data or explicit field definitions.

FieldItem

A single field in a Form, addressed by its key.

GroupItem

A labeled group of fields with its own column layout, placed in a Form.

TabsItem

A tab container holding one or more TabItem entries, placed in a Form.

TabItem

A single tab within a TabsItem.

FormItem

Represent a PEP 604 union type

Full Example#

 1
 2with bs.App(title="Form Demo", minsize=(700, 780), padding=20, gap=12) as app:
 3
 4    # Auto-generated fields from a data dict
 5    bs.Label("Auto-Generated Fields", font="heading-sm")
 6    bs.Form(
 7        data={
 8            "name": "Alice Smith",
 9            "email": "alice@example.com",
10            "age": 30,
11            "active": True,
12        },
13        horizontal="stretch",
14    )
15
16    # Multi-column layout
17    bs.Label("Multiple Columns", font="heading-sm")
18    bs.Form(
19        data={
20            "street": "123 Main St",
21            "city": "Springfield",
22            "state": "IL",
23            "zip": "62701",
24        },
25        col_count=2,
26        horizontal="stretch",
27    )
28
29    # Grouped fields with GroupItem
30    bs.Label("Grouped Fields", font="heading-sm")
31    bs.Form(
32        items=[
33            bs.GroupItem(
34                label="Contact",
35                col_count=2,
36                items=[
37                    bs.FieldItem(key="first_name", label="First Name"),
38                    bs.FieldItem(key="last_name",  label="Last Name"),
39                    bs.FieldItem(key="email",       label="Email"),
40                    bs.FieldItem(key="phone",       label="Phone"),
41                ],
42            ),
43        ],
44        horizontal="stretch",
45    )
46
47    # Tabbed layout with TabsItem
48    bs.Label("Tabbed Layout", font="heading-sm")
49    bs.Form(
50        items=[
51            bs.TabsItem(tabs=[
52                bs.TabItem(
53                    label="Account",
54                    items=[
55                        bs.FieldItem(key="username", label="Username"),
56                        bs.FieldItem(key="password", label="Password", dtype="password"),
57                    ],
58                ),
59                bs.TabItem(
60                    label="Profile",
61                    items=[
62                        bs.FieldItem(key="bio",     label="Bio",     editor="textarea"),
63                        bs.FieldItem(key="website", label="Website"),
64                    ],
65                ),
66            ]),
67        ],
68        horizontal="stretch",
69    )
70
71app.run()