Files
2025-11-29 17:58:08 +08:00

11 KiB

Textual Widget Gallery

Comprehensive examples of all built-in Textual widgets.

Basic Widgets

Label

Display static or dynamic text:

from textual.widgets import Label

# Simple label
yield Label("Hello World")

# With styling
yield Label("Important!", classes="highlight")

# With markup
yield Label("[bold]Bold[/] and [italic]italic[/]")

# Dynamic label with reactive
class DynamicLabel(Widget):
    message = reactive("Initial")
    
    def compose(self) -> ComposeResult:
        yield Label(self.message)

Static

Display Rich renderables:

from textual.widgets import Static
from rich.table import Table

table = Table()
table.add_column("Name")
table.add_column("Value")
table.add_row("Alpha", "100")

yield Static(table)

Button

Interactive buttons with variants:

from textual.widgets import Button

# Standard button
yield Button("Click me", id="action")

# Button variants
yield Button("Primary", variant="primary")
yield Button("Success", variant="success")
yield Button("Warning", variant="warning")
yield Button("Error", variant="error")

# Disabled button
yield Button("Disabled", disabled=True)

# Handle click
def on_button_pressed(self, event: Button.Pressed) -> None:
    if event.button.id == "action":
        self.action_perform()

Input Widgets

Input

Single-line text input:

from textual.widgets import Input

# Basic input
yield Input(placeholder="Enter text...", id="name")

# Password input
yield Input(placeholder="Password", password=True, id="pass")

# Validated input
yield Input(
    placeholder="Email",
    validators=[Email()],  # Built-in validators
    id="email"
)

# Handle submission
def on_input_submitted(self, event: Input.Submitted) -> None:
    value = event.value
    self.log(f"Submitted: {value}")

# Handle changes
def on_input_changed(self, event: Input.Changed) -> None:
    self.validate_live(event.value)

TextArea

Multi-line text editor:

from textual.widgets import TextArea

# Basic text area
yield TextArea(id="editor")

# With initial content
yield TextArea(
    text="Initial content\nLine 2",
    language="python",  # Syntax highlighting
    theme="monokai",
    id="code"
)

# Handle changes
def on_text_area_changed(self, event: TextArea.Changed) -> None:
    content = event.text_area.text

Select

Dropdown selection:

from textual.widgets import Select

# Basic select
options = [
    ("Option A", "a"),
    ("Option B", "b"),
    ("Option C", "c"),
]
yield Select(options=options, prompt="Choose...", id="choice")

# Handle selection
def on_select_changed(self, event: Select.Changed) -> None:
    value = event.value  # "a", "b", or "c"
    self.log(f"Selected: {value}")

Checkbox

Boolean input:

from textual.widgets import Checkbox

yield Checkbox("Enable feature", id="feature")

# Handle changes
def on_checkbox_changed(self, event: Checkbox.Changed) -> None:
    is_checked = event.value
    self.toggle_feature(is_checked)

RadioButton and RadioSet

Mutually exclusive options:

from textual.widgets import RadioButton, RadioSet

with RadioSet(id="size"):
    yield RadioButton("Small")
    yield RadioButton("Medium", value=True)  # Default
    yield RadioButton("Large")

# Handle selection
def on_radio_set_changed(self, event: RadioSet.Changed) -> None:
    selected = event.pressed.label
    self.log(f"Size: {selected}")

Switch

Toggle switch:

from textual.widgets import Switch

yield Switch(value=True, id="notifications")

# Handle toggle
def on_switch_changed(self, event: Switch.Changed) -> None:
    is_on = event.value
    self.toggle_notifications(is_on)

Data Display Widgets

DataTable

Tabular data with selection and sorting:

from textual.widgets import DataTable

table = DataTable(id="users")

# Add columns
table.add_columns("Name", "Age", "City")

# Add rows (returns row key)
row_key = table.add_row("Alice", 30, "NYC")
table.add_row("Bob", 25, "LA")

# Cursor control
table.cursor_type = "row"  # or "cell", "column", "none"

# Handle selection
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
    row_key = event.row_key
    row_data = event.row
    self.log(f"Selected: {row_data}")

# Update cells
table.update_cell(row_key, "Age", 31)

# Remove rows
table.remove_row(row_key)

# Sort
table.sort("Age", reverse=True)

Tree

Hierarchical data:

from textual.widgets import Tree

tree = Tree("Root", id="file-tree")

# Add nodes
root = tree.root
folder = root.add("Folder", expand=True)
folder.add_leaf("file1.txt")
folder.add_leaf("file2.txt")

subfolder = folder.add("Subfolder")
subfolder.add_leaf("nested.txt")

# Handle selection
def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
    node = event.node
    self.log(f"Selected: {node.label}")

# Expand/collapse
def on_tree_node_expanded(self, event: Tree.NodeExpanded) -> None:
    # Load children dynamically
    node = event.node
    self.load_children(node)

ListView

List with selection:

from textual.widgets import ListView, ListItem, Label

list_view = ListView(id="menu")

# Add items
list_view.append(ListItem(Label("Item 1")))
list_view.append(ListItem(Label("Item 2")))
list_view.append(ListItem(Label("Item 3")))

# Handle selection
def on_list_view_selected(self, event: ListView.Selected) -> None:
    item = event.item
    self.log(f"Selected: {item}")

Log / RichLog

Scrollable log output:

from textual.widgets import Log, RichLog

# Simple log
log = Log(id="output", auto_scroll=True)
log.write_line("Log entry")
log.write_lines(["Line 1", "Line 2"])

# Rich log with markup
rich_log = RichLog(id="rich-output", highlight=True)
rich_log.write("[bold green]Success![/]")
rich_log.write("[red]Error occurred[/]")

# Clear log
log.clear()

ProgressBar

Progress indicator:

from textual.widgets import ProgressBar

# Determinate progress
progress = ProgressBar(total=100, id="progress")
progress.advance(25)  # 25%
progress.update(progress=50)  # 50%

# Indeterminate progress
progress = ProgressBar(total=None)  # Animated spinner

Sparkline

Inline data visualization:

from textual.widgets import Sparkline

data = [1, 2, 3, 5, 8, 13, 21]
yield Sparkline(data, id="chart")

# Update data
sparkline = self.query_one(Sparkline)
sparkline.data = new_data

Navigation Widgets

Standard app chrome:

from textual.widgets import Header, Footer

def compose(self) -> ComposeResult:
    yield Header(show_clock=True)
    # ... content ...
    yield Footer()

TabbedContent / TabPane

Tabbed interface:

from textual.widgets import TabbedContent, TabPane

with TabbedContent(id="tabs"):
    with TabPane("Tab 1", id="tab1"):
        yield Label("Content 1")
    with TabPane("Tab 2", id="tab2"):
        yield Label("Content 2")
    with TabPane("Tab 3", id="tab3"):
        yield Label("Content 3")

# Handle tab changes
def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
    tab_id = event.pane.id
    self.log(f"Switched to {tab_id}")

ContentSwitcher

Programmatically switch content:

from textual.widgets import ContentSwitcher

with ContentSwitcher(initial="view1", id="switcher"):
    yield Label("View 1", id="view1")
    yield Label("View 2", id="view2")
    yield Label("View 3", id="view3")

# Switch views
def switch_view(self, view_id: str) -> None:
    switcher = self.query_one(ContentSwitcher)
    switcher.current = view_id

OptionList

Selectable list of options:

from textual.widgets import OptionList
from textual.widgets.option_list import Option

option_list = OptionList(
    Option("Option 1", id="opt1"),
    Option("Option 2", id="opt2"),
    Option("Option 3", id="opt3"),
)

# Handle selection
def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> None:
    option_id = event.option.id
    self.log(f"Selected: {option_id}")

Loading Widgets

LoadingIndicator

Spinning loader:

from textual.widgets import LoadingIndicator

# Show while loading
with LoadingIndicator():
    # Content loading...
    pass

# Or standalone
yield LoadingIndicator(id="loader")

Placeholder

Development placeholder:

from textual.widgets import Placeholder

# Quick placeholder during development
yield Placeholder("Chart goes here")
yield Placeholder(label="[b]User Profile[/]", variant="text")

Special Widgets

Markdown

Render markdown:

from textual.widgets import Markdown

markdown_content = """
# Title
This is **bold** and *italic*.

- Item 1
- Item 2

```python
code block

"""

yield Markdown(markdown_content, id="docs")


### DirectoryTree

File system browser:
```python
from textual.widgets import DirectoryTree

tree = DirectoryTree("/home/user", id="files")

# Handle file selection
def on_directory_tree_file_selected(self, event: DirectoryTree.FileSelected) -> None:
    file_path = event.path
    self.open_file(file_path)

Custom Widget Example

Create composite widgets:

from textual.widget import Widget
from textual.containers import Horizontal, Vertical

class UserCard(Widget):
    """Display user information."""
    
    def __init__(self, name: str, email: str, role: str) -> None:
        super().__init__()
        self.name = name
        self.email = email
        self.role = role
    
    def compose(self) -> ComposeResult:
        with Vertical(classes="card"):
            yield Label(self.name, classes="name")
            yield Label(self.email, classes="email")
            with Horizontal():
                yield Label(f"Role: {self.role}", classes="role")
                yield Button("Edit", variant="primary")
    
    DEFAULT_CSS = """
    UserCard {
        border: solid $primary;
        padding: 1;
        margin: 1;
    }
    
    UserCard .name {
        text-style: bold;
        color: $text;
    }
    
    UserCard .email {
        color: $text-muted;
    }
    """

# Use the widget
yield UserCard("Alice Smith", "alice@example.com", "Admin")

Widget Composition Patterns

Form Layout

def compose(self) -> ComposeResult:
    with Container(id="form"):
        yield Label("Registration Form")
        yield Input(placeholder="Name", id="name")
        yield Input(placeholder="Email", id="email")
        yield Input(placeholder="Password", password=True, id="pass")
        with Horizontal():
            yield Button("Submit", variant="primary")
            yield Button("Cancel")

Dashboard Layout

def compose(self) -> ComposeResult:
    yield Header()
    with Container(id="dashboard"):
        with Horizontal(classes="stats"):
            yield Static("[b]Users:[/] 1,234", classes="stat")
            yield Static("[b]Active:[/] 567", classes="stat")
            yield Static("[b]Revenue:[/] $12K", classes="stat")
        with Horizontal(classes="content"):
            with Vertical(id="sidebar"):
                yield Label("Menu")
                yield Button("Dashboard")
                yield Button("Users")
                yield Button("Settings")
            with Container(id="main"):
                yield DataTable()
        yield Footer()