TextArea#

A multi-line text input with optional label, placeholder, scrollbars, and undo/redo support.

TextArea — light theme TextArea — dark theme

Usage#

Basic usage#

bs.TextArea(placeholder="Start typing…")

Height#

Use height= to set the visible row count (default 4).

bs.TextArea(height=6)

Label and message#

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

bs.TextArea(
    label="Description",
    message="Markdown supported.",
    placeholder="Write a short description…",
    height=4,
)

Max length#

Set max_length= to cap the number of characters the user can type.

bs.TextArea(
    label="Bio",
    placeholder="Tell us about yourself…",
    max_length=200,
    message="Maximum 200 characters.",
)

Scrollbars#

Control when scrollbars appear with scrollbars=. The default 'auto' shows scrollbars only when content overflows.

bs.TextArea(scrollbars="auto")      # default — appears on overflow
bs.TextArea(scrollbars="vertical")  # always visible
bs.TextArea(scrollbars="both")      # horizontal + vertical, always
bs.TextArea(scrollbars="none")      # never shown
TextArea scrollbars — light theme TextArea scrollbars — dark theme

States#

bs.TextArea(value="Editable content.",  label="Normal")
bs.TextArea(value="Read-only content.", label="Read only", read_only=True)
TextArea states — light theme TextArea states — dark theme

Reactive binding#

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

content = bs.Signal("")
bs.TextArea(label="Notes", textsignal=content)
bs.Label(textsignal=content, accent="secondary")

Handling changes#

Use on_input() for real-time keystroke feedback or on_change() for commit-on-blur behaviour (fires when the user leaves the field).

ta = bs.TextArea(label="Notes")

# Fires on every edit
ta.on_input(lambda e: print(len(ta.value), "chars"))

# Fires on blur
ta.on_change(lambda e: save(ta.value))

# As a debounced Stream
ta.on_input().debounce(500).listen(lambda e: autosave(ta.value))

Undo and redo#

TextArea maintains a built-in undo/redo stack. Call undo() and redo() programmatically, or let the user use Ctrl+Z / Ctrl+Y.

ta = bs.TextArea()
bs.Button("Undo", on_click=lambda: ta.undo())
bs.Button("Redo", on_click=lambda: ta.redo())

Dirty tracking#

is_dirty is True after any edit since the last mark_saved() call. Use it to show unsaved-change indicators.

ta = bs.TextArea()
ta.on_modified(lambda e: print("dirty:", ta.is_dirty))

# After saving
ta.mark_saved()

Widget sizing#

All widgets accept self-placement kwargs via **kwargs. The parent container determines which options apply — stack-based parents use stack kwargs, grid-based parents use grid kwargs. Unrecognised keys are silently ignored.

Stack#

Used inside VStack, HStack, App, and other stack containers.

fill

Fill direction: 'x', 'y', 'both', or 'none'.

expand

Grow to consume extra space in the parent. True or False.

anchor

Alignment when the widget does not fill the available slot: 'n', 's', 'e', 'w', 'center', 'nw', etc.

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.

sticky

Alignment and fill within the grid cell. Any combination of 'n', 's', 'e', 'w' — e.g. 'ew' stretches horizontally, 'nsew' fills the entire cell.

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#

API#

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

TextArea

A multi-line text input with optional label, placeholder, and scrollbars.

Full Example#

 1
 2with bs.App(title="TextArea Demo", padding=20, gap=16) as app:
 3
 4    # Basic usage
 5    bs.Label("Basic", font="heading-sm")
 6    bs.TextArea(placeholder="Start typing…", height=3)
 7
 8    # Label and message
 9    bs.Label("Label and Message", font="heading-sm")
10    bs.TextArea(
11        label="Description",
12        message="Markdown supported.",
13        placeholder="Write a short description…",
14        height=4,
15    )
16
17    # Max length
18    bs.Label("Max Length", font="heading-sm")
19    bs.TextArea(
20        label="Bio",
21        placeholder="Tell us about yourself…",
22        max_length=200,
23        message="Maximum 200 characters.",
24        height=3,
25    )
26
27    # Scrollbars
28    bs.Label("Scrollbars", font="heading-sm")
29    with bs.HStack(gap=8):
30        bs.TextArea(label="auto",     scrollbars="auto",     height=3)
31        bs.TextArea(label="vertical", scrollbars="vertical", height=3)
32        bs.TextArea(label="none",     scrollbars="none",     height=3)
33
34    # Reactive binding
35    bs.Label("Reactive Binding", font="heading-sm")
36    content = bs.Signal("")
37    bs.TextArea(label="Notes", textsignal=content, height=3)
38    bs.Label(textsignal=content, accent="secondary")
39
40    # States
41    bs.Label("States", font="heading-sm")
42    bs.TextArea(value="Editable content.", label="Normal",    height=3)
43    bs.TextArea(value="Read-only content.", label="Read only", height=3, read_only=True)
44
45    # Undo / redo
46    bs.Label("Undo / Redo", font="heading-sm")
47    ta = bs.TextArea(label="Try editing, then click Undo", height=3)
48    with bs.HStack(gap=8):
49        bs.Button("Undo", on_click=lambda: ta.undo())
50        bs.Button("Redo", on_click=lambda: ta.redo())
51
52    # Dirty tracking
53    bs.Label("Dirty Tracking", font="heading-sm")
54    dirty_sig = bs.Signal("not modified")
55    ta2 = bs.TextArea(label="Edit me", height=3)
56    ta2.on_input(lambda e: dirty_sig.set("modified" if ta2.is_dirty else "not modified"))
57    bs.Label(textsignal=dirty_sig, accent="secondary")
58
59    # Handling changes
60    bs.Label("Handling Changes", font="heading-sm")
61    last = bs.Signal("(none)")
62    ta3 = bs.TextArea(label="Type and blur to commit", height=3)
63    ta3.on_change(lambda e: last.set(f"{len(ta3.value)} chars"))
64    bs.Label(textsignal=last, accent="secondary")
65
66app.run()