textual
Build terminal user interface (TUI) applications with the Textual framework. Use when creating new Textual apps, adding screens/widgets, styling with TCSS, handling events and reactivity, testing TUI apps, or any task involving "textual", "TUI", or terminal-based Python applications.
How do I install this agent skill?
npx skills add https://github.com/johnlarkin1/claude-code-extensions --skill textualIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides documentation and implementation examples for the Textual Python framework, used to build terminal user interfaces. The content is instructional and follows standard library practices with no security risks identified.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
- Runlayerwarn
2/4 files flagged
- ZeroLeakspass
Score: 93/100 · 2 sections analyzed
What does this agent skill do?
Textual TUI Framework
Build terminal applications with Textual's web-inspired architecture: App → Screen → Widget.
Quick Start
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
class MyApp(App):
CSS_PATH = "styles.tcss"
BINDINGS = [("q", "quit", "Quit"), ("d", "toggle_dark", "Dark Mode")]
def compose(self) -> ComposeResult:
yield Header()
yield Static("Hello, World!")
yield Footer()
def action_toggle_dark(self) -> None:
self.theme = "textual-dark" if self.theme == "textual-light" else "textual-light"
if __name__ == "__main__":
MyApp().run()
Core Concepts
Widget Lifecycle
__init__()→compose()→on_mount()→on_show()/on_hide()→on_unmount()
Reactivity
from textual.reactive import reactive, var
class MyWidget(Widget):
count = reactive(0) # Triggers refresh on change
internal = var("") # No automatic refresh
def watch_count(self, new_value: int) -> None:
"""Called when count changes."""
self.styles.background = "green" if new_value > 0 else "red"
def validate_count(self, value: int) -> int:
"""Constrain values."""
return max(0, min(100, value))
Events and Messages
from textual import on
from textual.message import Message
class MyWidget(Widget):
class Selected(Message):
def __init__(self, value: str) -> None:
self.value = value
super().__init__()
def on_click(self) -> None:
self.post_message(self.Selected("item"))
class MyApp(App):
# Handler naming: on_<widget>_<message>
def on_button_pressed(self, event: Button.Pressed) -> None:
self.log(f"Button {event.button.id} pressed")
@on(Button.Pressed, "#submit") # CSS selector filtering
def handle_submit(self) -> None:
pass
Data Flow
- Attributes down: Parent sets child properties directly
- Messages up: Child posts messages to parent via
post_message()
Screens
from textual.screen import Screen
class WelcomeScreen(Screen):
BINDINGS = [("escape", "app.pop_screen", "Back")]
def compose(self) -> ComposeResult:
yield Static("Welcome!")
yield Button("Continue", id="continue")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "continue":
self.app.push_screen("main")
class MyApp(App):
SCREENS = {"welcome": WelcomeScreen, "main": MainScreen}
def on_mount(self) -> None:
self.push_screen("welcome")
Custom Widgets
Simple Widget
class Greeting(Widget):
def render(self) -> RenderResult:
return "Hello, [bold]World[/bold]!"
Compound Widget
class LabeledButton(Widget):
DEFAULT_CSS = """
LabeledButton { layout: horizontal; height: auto; }
LabeledButton Label { width: 1fr; }
"""
def __init__(self, label: str, button_text: str) -> None:
self.label_text = label
self.button_text = button_text
super().__init__()
def compose(self) -> ComposeResult:
yield Label(self.label_text)
yield Button(self.button_text)
Focusable Widget
class Counter(Widget):
can_focus = True
BINDINGS = [("up", "increment", "+"), ("down", "decrement", "-")]
count = reactive(0)
def action_increment(self) -> None:
self.count += 1
Layout Patterns
Containers
from textual.containers import Horizontal, Vertical, Grid, VerticalScroll
def compose(self) -> ComposeResult:
with Vertical():
with Horizontal():
yield Button("Left")
yield Button("Right")
with VerticalScroll():
for i in range(100):
yield Label(f"Item {i}")
Grid CSS
Grid {
layout: grid;
grid-size: 3 2; /* columns rows */
grid-columns: 1fr 2fr 1fr;
grid-gutter: 1 2;
}
#wide { column-span: 2; }
Docking
#header { dock: top; height: 3; }
#sidebar { dock: left; width: 25; }
#footer { dock: bottom; height: 1; }
Workers (Async)
from textual import work
class MyApp(App):
@work(exclusive=True) # Cancels previous
async def fetch_data(self, url: str) -> None:
async with httpx.AsyncClient() as client:
response = await client.get(url)
self.query_one("#result").update(response.text)
@work(thread=True) # For sync APIs
def sync_operation(self) -> None:
result = blocking_call()
self.call_from_thread(self.update_ui, result)
Testing
async def test_app():
app = MyApp()
async with app.run_test() as pilot:
await pilot.press("enter")
await pilot.click("#button")
await pilot.pause() # Wait for messages
assert app.query_one("#status").render() == "Done"
Common Operations
# Query widgets
self.query_one("#id")
self.query_one(Button)
self.query(".class")
# CSS classes
widget.add_class("active")
widget.toggle_class("visible")
widget.set_class(condition, "active")
# Visibility
widget.display = True/False
# Mount/remove
self.mount(NewWidget())
widget.remove()
# Timers
self.set_interval(1.0, callback)
self.set_timer(5.0, callback)
# Exit
self.exit(return_code=0)
References
- Widget catalog and messages: See references/widgets.md
- CSS properties and selectors: See references/css.md
- Complete examples: See references/examples.md
- Official docs: https://textual.textualize.io/
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/johnlarkin1/claude-code-extensions/textual">View textual on skillZs</a>