Code of the Day
AdvancedText User Interfaces

Textual basics

Create a minimal Textual app with a Header, Footer, and Static widget, and handle a key binding.

UtilitiesAdvanced10 min read
Recommended first
By the end of this lesson you will be able to:
  • Create a Textual App subclass with Header, Footer, and Static widgets
  • Implement a compose() method to declare the widget tree
  • Bind a key to an action and handle it with an on_key handler
  • Explain why TUI output differs between a real terminal and the code runner

Textual applications are Python classes that subclass App. The framework manages the event loop, terminal rendering, and keyboard input. Your job is to declare the widget tree and respond to events.

Installing textual

pip install textual

Textual is not in the standard library. For a tool you distribute, add it to dependencies in your pyproject.toml.

The minimal app

Every Textual app has two required pieces:

  1. A compose() method that yields the widgets to display.
  2. A call to app.run() to start the event loop.
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static

class HelloApp(App):
    """A minimal TUI application."""

    BINDINGS = [("q", "quit", "Quit")]

    def compose(self) -> ComposeResult:
        yield Header()
        yield Static("Hello, TUI!", id="message")
        yield Footer()

if __name__ == "__main__":
    app = HelloApp()
    app.run()

Header() renders the app title (taken from the class name by default) in a bar at the top. Footer() renders the keybinding help bar at the bottom. Static displays a fixed string that does not change unless you update it programmatically.

BINDINGS is a class-level list of (key, action, description) tuples. The "quit" action is built in to App; pressing q calls it. The description appears in the Footer widget automatically.

Understanding compose()

compose() is a generator — it yields widget instances. Textual mounts them in the order they are yielded. Layout is controlled by CSS (inline or in a separate .tcss file). By default, widgets stack vertically.

For a more structured layout, wrap widgets in a container:

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

class TwoColumnApp(App):
    def compose(self) -> ComposeResult:
        yield Header()
        with self.Horizontal():
            yield Static("Left panel", id="left")
            yield Static("Right panel", id="right")
        yield Footer()

The with statement here is a context manager that tells textual these widgets are children of a Horizontal container.

Handling keys directly

For keys not mapped to named actions, implement on_key:

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

class KeyDemoApp(App):
    BINDINGS = [("q", "quit", "Quit")]

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

    def on_key(self, event: Key) -> None:
        self.query_one("#display", Static).update(
            f"You pressed: {event.key}"
        )

self.query_one("#display", Static) finds the widget with id="display" and casts it to Static. .update() replaces its content. The screen re-renders automatically.

Running in a real terminal

The code cells below show the structure, but textual cannot render in the browser sandbox. Save the code to a file and run it with python app.py in a real terminal:

Python — editable, runs in your browser

Textual ships with a development server: textual run --dev app.py reloads the app whenever you save the file. It also includes textual devtools, a separate terminal panel that shows events, logs, and the widget tree in real time — invaluable when debugging layout issues.

Where to go next

Next: interactive forms — textual's Input, Button, and Checkbox widgets, the reactive attribute pattern, and how event handlers wire widgets together.

Finished reading? Mark it complete to track your progress.

On this page