Textual basics
Create a minimal Textual app with a Header, Footer, and Static widget, and handle a key binding.
- 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 textualTextual 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:
- A
compose()method that yields the widgets to display. - 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:
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.