Code of the Day
AdvancedText User Interfaces

Interactive forms

Use Textual's Input, Button, and Checkbox widgets with the reactive attribute pattern and event handlers.

UtilitiesAdvanced5 min read
By the end of this lesson you will be able to:
  • Describe the Input, Button, and Checkbox widgets
  • Explain the reactive attribute pattern and how it drives re-renders
  • Wire widgets together using on_button_pressed and on_input_changed

Textual widgets are the building blocks of interactive forms: Input captures text, Button triggers actions, Checkbox toggles state. What makes them powerful is the reactive attribute system — when an attribute decorated with reactive() changes, the app automatically re-renders the parts of the UI that depend on it.

The reactive attribute pattern

from textual.app import App, ComposeResult
from textual.reactive import reactive
from textual.widgets import Header, Footer, Static

class CounterApp(App):
    count: reactive[int] = reactive(0)

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static(id="display")
        yield Footer()

    def watch_count(self, new_value: int) -> None:
        """Called automatically when count changes."""
        self.query_one("#display", Static).update(
            f"Count: {new_value}"
        )

reactive(0) declares count as a reactive integer with default 0. The method watch_count follows the naming convention watch_<attribute> — textual calls it whenever the attribute's value changes. No manual refresh, no event emission; the change propagates automatically.

Input — capturing text

Input renders a text field. The on_input_changed event fires on every keystroke; on_input_submitted fires when the user presses Enter:

from textual.widgets import Input, Static
from textual.app import App, ComposeResult

class SearchApp(App):
    def compose(self) -> ComposeResult:
        yield Input(placeholder="Type to search...", id="query")
        yield Static(id="result")

    def on_input_changed(self, event: Input.Changed) -> None:
        query = event.value
        self.query_one("#result", Static).update(
            f"Searching for: {query!r}"
        )

event.value contains the current text in the input field. The handler fires after every character, so the result updates live.

Button — triggering actions

Button renders a clickable button. The on_button_pressed event carries the button's id so one handler can distinguish multiple buttons:

from textual.widgets import Button, Static
from textual.app import App, ComposeResult

class ConfirmApp(App):
    def compose(self) -> ComposeResult:
        yield Static("Confirm action?", id="prompt")
        yield Button("Yes", id="yes", variant="success")
        yield Button("No", id="no", variant="error")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "yes":
            self.query_one("#prompt", Static).update("Confirmed.")
        else:
            self.query_one("#prompt", Static).update("Cancelled.")

variant controls the button colour. Textual ships with primary, success, warning, and error variants.

Checkbox — toggling state

Checkbox renders a labelled toggle. on_checkbox_changed fires when the state flips:

from textual.widgets import Checkbox, Static
from textual.app import App, ComposeResult

class OptionsApp(App):
    def compose(self) -> ComposeResult:
        yield Checkbox("Enable verbose output", id="verbose")
        yield Checkbox("Write log file", id="log")
        yield Static("Options: none selected", id="summary")

    def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
        verbose = self.query_one("#verbose", Checkbox).value
        log = self.query_one("#log", Checkbox).value
        selected = [
            k for k, v in [("verbose", verbose), ("log", log)] if v
        ]
        label = ", ".join(selected) if selected else "none selected"
        self.query_one("#summary", Static).update(f"Options: {label}")

Textual's event naming convention is consistent: every widget event is a class nested inside the widget class — Button.Pressed, Input.Changed, Checkbox.Changed. The handler method name is on_ followed by the snake_case of the full event name: on_button_pressed, on_input_changed, on_checkbox_changed.

Where to go next

Next: live dashboards — updating a DataTable on a timer with set_interval to build a self-refreshing display.

Finished reading? Mark it complete to track your progress.

On this page