Introduction
Textual is the most ambitious terminal UI framework in the Python ecosystem. Built on top of Rich, it brings web-app-style development (CSS styling, reactive state, async events) to the terminal. Applications can target both the TTY and the browser (via textual-web).
With over 26,000 GitHub stars, Textual powers modern CLI tools like Harlequin (SQL IDE), Posting (HTTP client), and Memray (memory profiler UI).
What Textual Does
Textual gives you widgets (Button, Input, DataTable, Tree, etc.), CSS-based styling, an async event loop, reactive state (watch properties and rerender automatically), and a full devtools stack. You write a class, compose widgets, style with CSS, and run anywhere Python runs.
Architecture Overview
[Textual App]
|
[Compose Tree]
Widgets & containers
|
[CSS Stylesheet]
Layout, colors, borders
|
[Event Loop (asyncio)]
on_mount / on_key / on_click
|
[Message Queue]
Reactive updates
|
[Driver]
+-- Terminal (Linux/macOS/Windows)
+-- Browser (textual-web)Self-Hosting & Configuration
from textual.app import App, ComposeResult
from textual.widgets import Input, DataTable, Header, Footer
from textual.containers import Vertical
class SearchApp(App):
CSS_PATH = "search.tcss"
BINDINGS = [("q", "quit", "Quit")]
def compose(self) -> ComposeResult:
yield Header()
with Vertical():
yield Input(placeholder="Search...", id="q")
yield DataTable(id="results")
yield Footer()
def on_mount(self) -> None:
table = self.query_one(DataTable)
table.add_columns("Name", "Stars")
async def on_input_submitted(self, event: Input.Submitted) -> None:
rows = await fetch_results(event.value)
table = self.query_one(DataTable)
table.clear()
for row in rows:
table.add_row(*row)
SearchApp().run()Key Features
- Widget library — Button, Input, DataTable, Tree, Tabs, and 20+ more
- CSS styling — real cascading stylesheets for terminals (.tcss)
- Reactive state — properties that trigger re-renders on change
- Async event loop — asyncio-native, play nicely with aiohttp/httpx
- DevTools — live reload + console for debugging
- Cross-target — run in terminal or browser via textual-web
- Theming — built-in dark/light themes, easy custom palettes
- Testable — Pilot object lets you script UI interactions in pytest
Comparison with Similar Tools
| Feature | Textual | Rich | urwid | Curses | Blessed |
|---|---|---|---|---|---|
| Widgets | 20+ | No | Many | Low-level | Low-level |
| CSS Styling | Yes | BBCode markup | Manual | No | No |
| Async | Yes | No | Limited | No | No |
| Web Target | Yes | No | No | No | No |
| Learning Curve | Moderate | Low | High | Very High | Moderate |
| Best For | Full TUI apps | Pretty CLI output | Legacy TUI | Custom TUI | Shell scripts |
FAQ
Q: Can Textual apps really run in a browser? A: Yes. textual-web serves your app over WebSockets and renders it with an HTML canvas. Same code, zero changes.
Q: Is Textual production ready? A: Yes (v0.x but actively used). Posting, Harlequin, and dozens of commercial tools ship Textual apps.
Q: How do I test a Textual app? A: Use App.run_test() which returns a Pilot for scripting keystrokes and mouse clicks. Combine with pytest.
Q: Does it work on Windows? A: Yes, including Windows Terminal and legacy cmd.exe (with reduced fidelity).
Sources
- GitHub: https://github.com/Textualize/textual
- Docs: https://textual.textualize.io
- Author: Will McGugan (Textualize)
- License: MIT