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

10 KiB

Textual Layout Patterns

Common layout recipes for Textual applications.

Layout Types

Vertical (Default)

Stack widgets vertically:

from textual.app import App, ComposeResult
from textual.widgets import Label

class VerticalApp(App):
    def compose(self) -> ComposeResult:
        yield Label("Top")
        yield Label("Middle")
        yield Label("Bottom")

# Or explicit CSS
CSS = """
Screen {
    layout: vertical;
}
"""

Horizontal

Arrange widgets side-by-side:

from textual.containers import Horizontal

def compose(self) -> ComposeResult:
    with Horizontal():
        yield Label("Left")
        yield Label("Center")
        yield Label("Right")

# Or via CSS
CSS = """
Horizontal {
    height: 100%;
}

Horizontal > Label {
    width: 1fr;  /* Equal distribution */
}
"""

Grid

Create grid layouts:

class GridApp(App):
    CSS = """
    Screen {
        layout: grid;
        grid-size: 3 2;  /* 3 columns, 2 rows */
        grid-gutter: 1;
    }
    
    .cell {
        border: solid $accent;
        height: 100%;
    }
    """
    
    def compose(self) -> ComposeResult:
        for i in range(6):
            yield Label(f"Cell {i+1}", classes="cell")

Advanced grid with spanning:

CSS = """
Screen {
    layout: grid;
    grid-size: 4;  /* 4 columns, auto rows */
}

#header {
    column-span: 4;  /* Spans all columns */
}

#sidebar {
    row-span: 2;  /* Spans 2 rows */
}
"""

Dock Layout

Dock widgets to edges:

from textual.widgets import Header, Footer

class DockedApp(App):
    def compose(self) -> ComposeResult:
        yield Header()  # Docked to top
        yield Label("Content")  # Takes remaining space
        yield Footer()  # Docked to bottom

# Custom docking
CSS = """
#sidebar {
    dock: left;
    width: 30;
}

#toolbar {
    dock: top;
    height: 3;
}
"""

Common Patterns

Split Screen (Vertical)

Two panels side-by-side:

class SplitScreen(App):
    CSS = """
    Screen {
        layout: horizontal;
    }
    
    #left-panel {
        width: 30%;
        border-right: solid $accent;
    }
    
    #right-panel {
        width: 70%;
    }
    """
    
    def compose(self) -> ComposeResult:
        with Container(id="left-panel"):
            yield Label("Sidebar")
        with Container(id="right-panel"):
            yield Label("Main content")

Split Screen (Horizontal)

Two panels stacked:

class SplitScreenHorizontal(App):
    CSS = """
    Screen {
        layout: vertical;
    }
    
    #top-panel {
        height: 50%;
        border-bottom: solid $accent;
    }
    
    #bottom-panel {
        height: 50%;
    }
    """
    
    def compose(self) -> ComposeResult:
        with Container(id="top-panel"):
            yield Label("Top content")
        with Container(id="bottom-panel"):
            yield Label("Bottom content")

Three-Column Layout

Classic sidebar-content-sidebar:

class ThreeColumn(App):
    CSS = """
    Screen {
        layout: horizontal;
    }
    
    #left-sidebar {
        width: 20;
    }
    
    #content {
        width: 1fr;  /* Take remaining space */
    }
    
    #right-sidebar {
        width: 25;
    }
    """
    
    def compose(self) -> ComposeResult:
        with Container(id="left-sidebar"):
            yield Label("Menu")
        with Container(id="content"):
            yield Label("Main")
        with Container(id="right-sidebar"):
            yield Label("Info")

Dashboard Grid

Grid-based dashboard:

class Dashboard(App):
    CSS = """
    Screen {
        layout: grid;
        grid-size: 2 3;  /* 2 columns, 3 rows */
        grid-gutter: 1 2;  /* vertical horizontal */
    }
    
    #header {
        column-span: 2;
        height: 3;
    }
    
    .metric-card {
        border: solid $primary;
        padding: 1;
    }
    """
    
    def compose(self) -> ComposeResult:
        yield Header(id="header")
        yield Static("Users: 1,234", classes="metric-card")
        yield Static("Revenue: $12K", classes="metric-card")
        yield Static("Growth: +15%", classes="metric-card")
        yield Static("Active: 567", classes="metric-card")

Centered Content

Center content horizontally and vertically:

class CenteredApp(App):
    CSS = """
    Screen {
        align: center middle;
    }
    
    #dialog {
        width: 60;
        height: 20;
        border: thick $accent;
        padding: 2;
        background: $surface;
    }
    """
    
    def compose(self) -> ComposeResult:
        with Container(id="dialog"):
            yield Label("Centered Dialog")
            yield Button("OK")

Scrollable Content

Handle overflow with scrolling:

from textual.containers import ScrollableContainer

class ScrollableApp(App):
    CSS = """
    #content {
        height: 100%;
        border: solid $primary;
    }
    """
    
    def compose(self) -> ComposeResult:
        with ScrollableContainer(id="content"):
            for i in range(100):
                yield Label(f"Line {i+1}")

Tabbed Interface

Tab-based navigation:

from textual.widgets import TabbedContent, TabPane

class TabbedApp(App):
    def compose(self) -> ComposeResult:
        with TabbedContent():
            with TabPane("Dashboard"):
                yield Label("Dashboard content")
            with TabPane("Users"):
                yield Label("Users content")
            with TabPane("Settings"):
                yield Label("Settings content")

Sizing Strategies

Fixed Sizes

Absolute dimensions:

#widget {
    width: 40;   /* 40 columns */
    height: 20;  /* 20 rows */
}

Fractional Units

Proportional sizing:

#sidebar {
    width: 1fr;  /* 1 part */
}

#content {
    width: 3fr;  /* 3 parts (3x sidebar) */
}

Percentage

Relative to parent:

#widget {
    width: 50%;   /* Half of parent width */
    height: 100%; /* Full parent height */
}

Auto Sizing

Size to content:

#widget {
    width: auto;  /* Width matches content */
    height: auto; /* Height matches content */
}

Min/Max Constraints

Bounded sizing:

#widget {
    width: 1fr;
    min-width: 30;
    max-width: 80;
}

Spacing and Alignment

Padding

Space inside widget:

#widget {
    padding: 1;          /* All sides */
    padding: 1 2;        /* Vertical Horizontal */
    padding: 1 2 1 2;    /* Top Right Bottom Left */
    padding-top: 1;      /* Individual sides */
}

Margin

Space outside widget:

#widget {
    margin: 1;
    margin: 0 2;         /* No vertical, 2 horizontal */
    margin-left: 1;
}

Alignment

Position within container:

Container {
    align: center middle;     /* Horizontal Vertical */
    align: left top;
    align: right bottom;
}

/* Content alignment (for containers) */
Container {
    content-align: center middle;
}

Responsive Layouts

Container Queries

Adjust based on container size:

class ResponsiveApp(App):
    CSS = """
    Screen {
        layout: horizontal;
    }
    
    /* Default mobile layout */
    #content {
        layout: vertical;
    }
    
    /* Desktop layout when width > 80 */
    Screen:width-gt-80 #content {
        layout: horizontal;
    }
    """

Conditional Layouts

Switch layouts based on screen size:

def compose(self) -> ComposeResult:
    if self.size.width > 100:
        # Wide layout
        with Horizontal():
            yield self.make_sidebar()
            yield self.make_content()
    else:
        # Narrow layout
        with Vertical():
            yield self.make_content()

Advanced Patterns

Modal Overlay

Centered modal dialog:

from textual.screen import ModalScreen
from textual.containers import Container

class Modal(ModalScreen[bool]):
    CSS = """
    Modal {
        align: center middle;
    }
    
    #dialog {
        width: 50;
        height: 15;
        border: thick $accent;
        background: $surface;
        padding: 1;
    }
    """
    
    def compose(self) -> ComposeResult:
        with Container(id="dialog"):
            yield Label("Are you sure?")
            with Horizontal():
                yield Button("Yes", variant="primary")
                yield Button("No", variant="error")

Sidebar Toggle

Collapsible sidebar:

class SidebarApp(App):
    show_sidebar = reactive(True)
    
    CSS = """
    #sidebar {
        width: 30;
        transition: width 200ms;
    }
    
    #sidebar.hidden {
        width: 0;
        display: none;
    }
    """
    
    def watch_show_sidebar(self, show: bool) -> None:
        sidebar = self.query_one("#sidebar")
        sidebar.set_class(not show, "hidden")

Masonry Layout

Staggered grid:

class MasonryLayout(App):
    CSS = """
    Screen {
        layout: grid;
        grid-size: 3;
        grid-gutter: 1;
    }
    
    .card {
        height: auto;
        border: solid $primary;
        padding: 1;
    }
    
    .card.tall {
        row-span: 2;
    }
    """
    
    def compose(self) -> ComposeResult:
        yield Static("Short card", classes="card")
        yield Static("Tall card\n\n\n", classes="card tall")
        yield Static("Short", classes="card")

Split Resizable

Adjustable split panels:

class ResizableSplit(App):
    left_width = reactive(30)
    
    CSS = """
    #left {
        width: var(--left-width);
    }
    
    #right {
        width: 1fr;
    }
    
    #divider {
        width: 1;
        background: $accent;
    }
    """
    
    def watch_left_width(self, width: int) -> None:
        self.set_var("left-width", width)

Layout Debugging

Use borders to visualize layout:

* {
    border: solid red;  /* Temporary debugging */
}

Container {
    border: solid blue;
}

Widget {
    border: solid green;
}

Use Textual devtools:

textual run --dev app.py

Add debug info to widgets:

def compose(self) -> ComposeResult:
    yield Label(f"Size: {self.size}")
    yield Label(f"Region: {self.region}")