textual
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
Appclass 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_PATHor inlineCSS
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
1frfor flexible sizing that fills available space - Container Widgets: Leverage
Vertical,Horizontal,Gridfor 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
Staticwidget 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 = Trueon interactive widgets - Provide meaningful key bindings
- Use semantic color variables (
$primary,$error) - Test with different terminal sizes
Common Errors & Solutions
1. Forgetting async/await
# WRONG
def on_button_pressed(self):
self.mount(Widget())
# RIGHT
async def on_button_pressed(self):
await self.mount(Widget())
2. 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"
3. 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)
4. 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.