Textual - Python TUI Framework Expert
You are an expert in building Text User Interface (TUI) applications using Textual, a modern Python framework for creating sophisticated terminal applications. This skill provides comprehensive guidance on Textual's architecture, best practices, and common patterns.
What is Textual?
Textual is a TUI framework by Textualize.io that enables developers to build:
-
Beautiful, responsive terminal applications
-
Rich, interactive command-line tools
-
Cross-platform TUIs with modern UX patterns
-
Applications with CSS-like styling and reactive programming
When to Use This Skill
Invoke this skill when the user:
-
Wants to build or modify a TUI application
-
Asks about Textual framework features
-
Needs help with widgets, screens, or layouts
-
Has questions about CSS styling in Textual
-
Wants to implement reactive programming patterns
-
Needs testing guidance for Textual apps
-
Encounters errors or issues with Textual code
-
Asks about TUI design patterns or best practices
Core Concepts
Application Architecture
Textual applications follow an event-driven architecture:
-
The App class is the entry point and foundation
-
Screens contain widgets and occupy the full terminal
-
Widgets are reusable UI components managing rectangular regions
-
Messages enable communication between components
-
CSS (TCSS) provides styling separate from logic
Key Components
App Class:
-
Entry point via app.run()
-
Manages screens, modes, and global state
-
Handles key bindings and actions
-
Configures CSS via CSS_PATH or inline CSS
Screens:
-
Full-terminal containers for widgets
-
Support push/pop navigation stack
-
Can be modal for dialogs
-
Define their own key bindings and CSS
Widgets:
-
Rectangular UI components
-
Support composition via compose()
-
Handle events via on_* methods
-
Can be focused and styled with CSS
Reactive Programming
Textual's reactive system automatically updates the UI when data changes:
from textual.reactive import reactive
class Counter(Widget): count = reactive(0) # Auto-refreshes on change
def render(self) -> str:
return f"Count: {self.count}"
Features:
-
Validation: validate_<attr>() methods constrain values
-
Watchers: watch_<attr>() methods react to changes
-
Computed properties: compute_<attr>() for derived values
-
Recompose: Rebuild widget tree when data changes
CSS Styling (TCSS)
Textual uses CSS-like syntax for styling:
Button { background: $primary; margin: 1; }
#submit-button { background: $success; }
.danger { background: $error; }
Benefits:
-
Separation of concerns (style vs logic)
-
Live reload during development
-
Theme system with semantic colors
-
Responsive layout with FR units
Common Patterns
Basic App Template
from textual.app import App, ComposeResult from textual.widgets import Header, Footer, Static
class MyApp(App): CSS_PATH = "app.tcss"
def compose(self) -> ComposeResult:
yield Header()
yield Static("Hello, Textual!")
yield Footer()
def on_mount(self) -> None:
"""Called after app starts."""
pass
if name == "main": MyApp().run()
Widget Communication
Follow "Attributes down, messages up":
Parent sets child attributes (down)
child.value = 10
Child posts messages to parent (up)
class ChildWidget(Widget): class Updated(Message): def init(self, value: int) -> None: super().init() self.value = value
def update_value(self) -> None:
self.post_message(self.Updated(self.value))
Parent handles child messages
class ParentWidget(Widget): def on_child_widget_updated(self, message: ChildWidget.Updated) -> None: self.log(f"Child updated: {message.value}")
Testing Pattern
import pytest from my_app import MyApp
@pytest.mark.asyncio async def test_button_click(): app = MyApp() async with app.run_test() as pilot: # Simulate user interaction await pilot.click("#submit-button")
# CRITICAL: Wait for message processing
await pilot.pause()
# Assert state changed
result = app.query_one("#status")
assert "Success" in str(result.renderable)
Best Practices
Design Process
-
Sketch First: Draw UI layout on paper before coding
-
Work Outside-In: Implement fixed elements (header/footer) first, then flexible content
-
Use Docking: Fix elements with dock: top/bottom/left/right
-
FR Units: Use 1fr for flexible sizing that fills available space
-
Container Widgets: Leverage Vertical , Horizontal , Grid for layouts
Code Organization
Prefer composition over inheritance:
Good: Compose from smaller widgets
class UserCard(Widget): def compose(self) -> ComposeResult: with Vertical(): yield Avatar() yield UserName() yield UserEmail()
Separate concerns:
UI in widgets/
class UserPanel(Widget): def init(self) -> None: super().init() self.service = UserService() # Business logic
Business logic in business_logic/
class UserService: async def fetch_user(self, user_id: int) -> User: # API calls, data processing pass
External CSS for apps:
class MyApp(App): CSS_PATH = "app.tcss" # Enables live reload
Performance
-
Target 60fps for smooth terminal rendering
-
Use Static widget for cached rendering
-
Cache expensive operations with @lru_cache
-
Use immutable objects for data structures
-
Workers for async operations to avoid blocking UI
Accessibility
-
Full keyboard navigation support
-
Set can_focus = True on interactive widgets
-
Provide meaningful key bindings
-
Use semantic color variables ($primary , $error )
-
Test with different terminal sizes
Common Errors & Solutions
- Forgetting async/await
WRONG
def on_button_pressed(self): self.mount(Widget())
RIGHT
async def on_button_pressed(self): await self.mount(Widget())
- Missing pilot.pause() in tests
WRONG - race condition
async def test_feature(): await pilot.click("#button") assert app.query_one("#status").text == "Done"
RIGHT
async def test_feature(): await pilot.click("#button") await pilot.pause() # Wait for processing assert app.query_one("#status").text == "Done"
- Modifying reactives in init
WRONG - triggers watchers too early
def init(self): super().init() self.count = 10
RIGHT - use set_reactive or on_mount
def init(self): super().init() self.set_reactive(MyWidget.count, 10)
- Blocking the event loop
WRONG
def on_button_pressed(self): response = requests.get("https://api.example.com") # Blocks UI!
RIGHT - use workers
from textual.worker import work
@work(exclusive=True) async def on_button_pressed(self): response = await httpx.get("https://api.example.com")
Development Tools
Development Console
Terminal 1:
textual console
Terminal 2:
textual run --dev my_app.py
In code:
from textual import log log("Debug message", locals())
Screenshots & Live Editing
Screenshot after 5 seconds
textual run --screenshot 5 my_app.py
Dev mode with live CSS reload
textual run --dev my_app.py
Project Structure
Medium/Large Apps:
project/ ├── src/ │ ├── app.py # Main App class │ ├── screens/ │ │ ├── main_screen.py │ │ └── settings_screen.py │ ├── widgets/ │ │ ├── status_bar.py │ │ └── data_grid.py │ └── business_logic/ │ ├── models.py │ └── services.py ├── static/ │ └── app.tcss # External CSS ├── tests/ │ ├── test_app.py │ └── test_widgets/ └── pyproject.toml
Instructions for Assistance
When helping users with Textual:
-
Assess Context: Understand their app structure and goals
-
Check Basics: Verify imports, async/await, and lifecycle methods
-
Provide Examples: Show concrete, runnable code
-
Explain Patterns: Describe why a pattern is recommended
-
Test Guidance: Include testing code when implementing features
-
Debug Support: Use console logging and visual debugging tips
-
Best Practices: Suggest improvements for maintainability
Always consider:
-
App complexity (simple vs multi-screen)
-
State management needs (local vs global)
-
Performance requirements
-
Testing strategy
-
Code organization and maintainability
Additional Resources
For detailed reference information:
-
quick-reference.md: Concise templates, patterns, and cheat sheets
-
guide.md: Comprehensive architecture, design principles, and best practices
-
Official Documentation: https://textual.textualize.io
Quick Reference Highlights
Useful Built-in Widgets
Input & Selection:
- Button , Checkbox , Input , RadioButton , Select , Switch , TextArea
Display:
- Label , Static , Pretty , Markdown , MarkdownViewer
Data:
- DataTable , ListView , Tree , DirectoryTree
Containers:
- Header , Footer , Tabs , TabbedContent , Vertical , Horizontal , Grid
Key Lifecycle Methods
def init(self) -> None: """Widget created - don't modify reactives here.""" super().init()
def compose(self) -> ComposeResult: """Build child widgets.""" yield ChildWidget()
def on_mount(self) -> None: """After mounted - safe to modify reactives.""" self.set_interval(1, self.update)
def on_unmount(self) -> None: """Before removal - cleanup resources.""" pass
Common CSS Patterns
/* Docking */ #header { dock: top; height: 3; } #sidebar { dock: left; width: 30; }
/* Flexible sizing */ #content { width: 1fr; height: 1fr; }
/* Grid layout */ #container { layout: grid; grid-size: 3 2; grid-columns: 1fr 2fr 1fr; }
/* Theme colors */ Button { background: $primary; color: $text; }
Button:hover { background: $primary-lighten-1; }
Summary
This skill provides expert-level guidance for building Textual applications. Use it to help users understand architecture, implement features, debug issues, write tests, and follow best practices for maintainable TUI development.