Claude
Skills
Sign in
Back

textual-widget-development

Included with Lifetime
$97 forever

Designs and implements custom Textual widgets with composition, styling, and lifecycle management. Use when creating reusable widget components, composing widgets from built-in components, implementing widget lifecycle (on_mount, on_unmount), handling widget state, and testing widgets. Covers custom widgets extending Static, Container, and building complex widget hierarchies.

General

What this skill does


# Textual Widget Development

## Purpose
Build reusable, composable Textual widgets that follow functional principles, proper lifecycle management, and type safety. Widgets are the fundamental building blocks of Textual applications.

## Quick Start

```python
from textual.app import ComposeResult
from textual.widgets import Static, Container
from textual.containers import Vertical

class SimpleWidget(Static):
    """A simple reusable widget."""

    DEFAULT_CSS = """
    SimpleWidget {
        height: auto;
        border: solid $primary;
        padding: 1;
    }
    """

    def __init__(self, title: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._title = title

    def render(self) -> str:
        """Render widget content."""
        return f"Title: {self._title}"

# Use in app:
class MyApp(App):
    def compose(self) -> ComposeResult:
        yield SimpleWidget("Hello")
```

## Instructions

### Step 1: Choose Widget Base Class

Select appropriate base class based on widget purpose:

```python
from textual.widgets import Static, Container, Input, Button
from textual.containers import Vertical, Horizontal, Container as GenericContainer

# For custom content/display - Simple widgets
class StatusWidget(Static):
    """Displays status information."""
    pass

# For layout/composition - Container widgets
class DashboardWidget(Container):
    """Composes multiple child widgets."""
    pass

# Built-in widgets (ready to use)
# - Static: Display text/rich content
# - Input: Text input field
# - Button: Clickable button
# - Label: Static label
# - Select: Dropdown selector
# - DataTable: Tabular data
# - Tree: Hierarchical data
```

**Guidelines:**
- Use `Static` for display-only content
- Use `Container` when you need to compose child widgets
- Use built-in widgets first before creating custom ones
- Create custom widgets only when built-in options don't fit

### Step 2: Define Widget Initialization and Configuration

Implement `__init__` with proper type hints and parent class initialization:

```python
from typing import ClassVar
from textual.app import ComposeResult
from textual.widgets import Static

class ConfigurableWidget(Static):
    """Widget with configurable parameters."""

    DEFAULT_CSS = """
    ConfigurableWidget {
        height: auto;
        border: solid $primary;
        padding: 1;
    }

    ConfigurableWidget .header {
        background: $boost;
        text-style: bold;
    }
    """

    # Class constants
    BORDER_COLOR: ClassVar[str] = "$primary"

    def __init__(
        self,
        title: str,
        content: str = "",
        *,
        name: str | None = None,
        id: str | None = None,  # noqa: A002
        classes: str | None = None,
        variant: str = "default",
    ) -> None:
        """Initialize widget.

        Args:
            title: Widget title.
            content: Initial content.
            name: Widget name.
            id: Widget ID for querying.
            classes: CSS classes to apply.
            variant: Visual variant (default, compact, etc).

        Always pass **kwargs to parent:
            super().__init__(name=name, id=id, classes=classes)
        """
        super().__init__(name=name, id=id, classes=classes)
        self._title = title
        self._content = content
        self._variant = variant
```

**Important Rules:**
- Always call `super().__init__()` with name, id, classes
- Store configuration in instance variables (prefix with `_`)
- Use type hints for all parameters
- Document all parameters with docstrings
- Use keyword-only arguments (after `*`) for optional parameters

### Step 3: Implement Widget Composition

For complex widgets that contain child widgets:

```python
from textual.app import ComposeResult
from textual.containers import Vertical, Horizontal
from textual.widgets import Static, Button, Label

class CompositeWidget(Vertical):
    """Widget that composes multiple child widgets."""

    DEFAULT_CSS = """
    CompositeWidget {
        height: auto;
        border: solid $primary;
    }

    CompositeWidget .header {
        height: 3;
        background: $boost;
        text-style: bold;
    }

    CompositeWidget .content {
        height: 1fr;
        overflow: auto;
    }

    CompositeWidget .footer {
        height: auto;
        border-top: solid $primary;
    }
    """

    def __init__(self, title: str, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._title = title
        self._items: list[str] = []

    def compose(self) -> ComposeResult:
        """Compose child widgets.

        Yields:
            Child widgets in order they should appear.
        """
        # Header
        yield Static(f"Title: {self._title}", classes="header")

        # Content area with items
        yield Vertical(
            Static(
                "Content area" if not self._items else "\n".join(self._items),
                id="content-area",
            ),
            classes="content",
        )

        # Footer with buttons
        yield Horizontal(
            Button("Add", id="btn-add", variant="primary"),
            Button("Remove", id="btn-remove"),
            classes="footer",
        )

    async def on_mount(self) -> None:
        """Initialize after composition."""
        # Can now query child widgets
        content = self.query_one("#content-area", Static)
        content.update("Initialized")

    async def add_item(self, item: str) -> None:
        """Add item to widget."""
        self._items.append(item)
        content = self.query_one("#content-area", Static)
        content.update("\n".join(self._items))
```

**Composition Pattern:**
- Override `compose()` to yield child widgets
- Use containers (Vertical, Horizontal) for layout
- Use `on_mount()` after children are mounted
- Query children by ID using `self.query_one()`

### Step 4: Implement Widget Rendering

For display widgets using `render()`:

```python
from rich.console import Console
from rich.table import Table
from rich.text import Text
from textual.widgets import Static

class RichWidget(Static):
    """Widget that renders Rich objects."""

    def __init__(self, data: dict, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._data = data

    def render(self) -> str | Text | Table:
        """Render widget content as Rich object.

        Returns:
            str, Text, or Rich-renderable object.
            Textual converts to displayable content.
        """
        # Simple text
        return f"Data: {self._data}"

        # Rich Text with styling
        text = Text()
        text.append("Status: ", style="bold")
        text.append(self._data.get("status", "unknown"), style="green")
        return text

        # Rich Table
        table = Table(title="Data")
        table.add_column("Key", style="cyan")
        table.add_column("Value", style="magenta")
        for key, value in self._data.items():
            table.add_row(key, str(value))
        return table
```

**Rendering Methods:**
1. `render()` - Return displayable content
2. `update(content)` - Update rendered content
3. `refresh()` - Force re-render

### Step 5: Add Widget Lifecycle Methods

Implement lifecycle hooks for initialization and cleanup:

```python
class LifecycleWidget(Static):
    """Widget with full lifecycle implementation."""

    def __init__(self, **kwargs: object) -> None:
        super().__init__(**kwargs)
        self._initialized = False

    async def on_mount(self) -> None:
        """Called when widget is mounted to DOM.

        Use for:
        - Initializing state
        - Starting background tasks
        - Loading data
        - Querying sibling widgets
        """
        self._initialized = True
        self.update("Widget mounted and ready")

        # Start background task
        self.app.run_worker(self._background_work())

    async def _background_work(self) -> None:
        """Background

Related in General