Textual
Use when work is primarily Textual TUI, terminal workflow, or tests for Textual widgets and screens.
Boundary
Use for:
- Textual app structure and widget composition
- layout and
.tcssstyling - bindings, actions, focus, interactive behavior
- screens, dialogs, menus, drawers, navigation flows
- headless functional testing with
run_test()
Pair with:
pythonfor general Python conventions, typing, project workflowrichwhen renderables, terminal formatting, or console UX matter outside TUIqualitywhen interaction regressions or state bugs need stronger tests
Reference Map
references/app-structure.md-- app shape, widgets, reactivity, messages, screens, state boundariesreferences/widgets.md-- common widgets, tables, forms, containers, custom widget guidancereferences/widget-development.md-- custom widget patterns: base class selection, composition, lifecycle, advanced compositionsreferences/layout-and-styling.md--.tcss, containers, layout, spacing, ids, classes, visual structure, themes, colorsreferences/reactive-programming.md-- reactive attrs, watchers, computed props, valid, complex state,recomposereferences/interactivity.md-- bindings, actions, focus, mouse and keyboard handling, interaction patternsreferences/testing.md-- headless tests, full Pilot API, resize, animations, workers, assert patterns for complex widgets
Assets
assets/app.py-- small Textual app with bindings, dialog-like screen, stable selectors for testsassets/app.tcss-- matching.tcssfile for example appassets/test_app.py-- headless functional tests usingrun_test()
What Stays Here
Keep this file focused on defaults and guardrails.
- keep here: app design defaults, testing stance, review cues
- move to refs: widget catalogs, long examples, styling details, specific test recipes
- use assets for copyable app and test skeletons over growing giant code blocks in refs
Core Defaults
- widgets small, responsibility-driven
- styling in
.tcss, not large inline CSS strings - semantic ids/classes so tests and styles have stable targets
- reactive state only for values that genuinely affect UI
- watchers small, explicit; heavy logic out of
watch_*methods - prefer
action_*+BINDINGSfor keyboard behavior - messages/events explicit; no smuggling state through globals
- screens or focused containers for dialog-like flows, not one giant app class
- stable widget ids/classes so styling, querying, tests align
- prefer built-in widgets/messages before inventing custom abstractions
- test interactions headlessly with
run_test()over only checking impl details - always call
super().__init__(name=name, id=id, classes=classes)in custom widget__init__ - replace list/dict entirely to trigger watchers --
.append()won't firewatch_* - frozen dataclasses for immutable reactive data points
Quick Start
from textual.app import App, ComposeResult
from textual.widgets import Footer, Header, Label
class DemoApp(App):
CSS_PATH = "app.tcss"
def compose(self) -> ComposeResult:
yield Header()
yield Label("Hello, Textual!", id="status")
yield Footer()
if __name__ == "__main__":
DemoApp().run()
Testing Defaults
- use
async with app.run_test() as pilot:for functional tests - call
await pilot.pause()after interactions that queue updates - use
await pilot.wait_for_animation()when animation timing matters - use
await pilot.wait_for_scheduled_animations()for all scheduled animations - use
await pilot.app.workers.wait_for_complete()for worker-driven flows - assert both user-visible state and underlying app state when useful
- configure pytest with
asyncio_mode = "auto"-- avoids@pytest.mark.asyncioon every test - use
pilot.app.query_one("#id", WidgetType)for typed querying - test different terminal sizes with
run_test(size=(w, h))
For deeper patterns, load references/testing.md.
Reactive Defaults
- declare type:
attr: reactive[Type] = reactive(default) - use
init=Falsewhen initializing in__init__; omit when reactive sets default - use
recompose=Truewhen attribute change should rebuild child widget tree - use
layout=Truewhen attribute change affects size/position - watcher signature:
watch_attr(self, old: T, new: T) -> None - computed: use
@propertyfor derived values; update via watcher when dependency changes - for valid: constrain in
watch_*, revert or clamp value there
For full patterns, load references/reactive-programming.md.
Widget Development Defaults
- extend
Staticfor display-only content; extendContainer/Vertical/Horizontalfor composition - put
DEFAULT_CSSon class for self-contained defaults - use keyword-only args (after
*) forid,name,classes - always pass
name,id,classestosuper().__init__() - store config in
_prefixedinstance vars; never in class vars that aren'tClassVar - custom messages: define as nested class; use
post_message()from child, handle in parent - messages flow UP -- parents handle, children emit; attributes flow DOWN -- parents set child attrs
For deep patterns, load references/widget-development.md.
- do not bury most of app in one monolithic
Appclass - do not rely on fragile widget order when ids or classes can make tests stable
- do not mix layout, styling, and interaction logic into same method
- do not overuse reactivity for one-off imperative updates
- do not make keyboard shortcuts undocumented or inconsistent with visible UI
- do not write Textual tests that only assert internal methods were called
Review Focus
- widget boundaries clear, reusable
- layout and styling targets stable
- bindings and actions discoverable, consistent
- dialogs, menus, drawers have sensible focus and close behavior
- tests cover real interaction flows, not only internal state