# Textual Layout Patterns Common layout recipes for Textual applications. ## Layout Types ### Vertical (Default) Stack widgets vertically: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```css #widget { width: 40; /* 40 columns */ height: 20; /* 20 rows */ } ``` ### Fractional Units Proportional sizing: ```css #sidebar { width: 1fr; /* 1 part */ } #content { width: 3fr; /* 3 parts (3x sidebar) */ } ``` ### Percentage Relative to parent: ```css #widget { width: 50%; /* Half of parent width */ height: 100%; /* Full parent height */ } ``` ### Auto Sizing Size to content: ```css #widget { width: auto; /* Width matches content */ height: auto; /* Height matches content */ } ``` ### Min/Max Constraints Bounded sizing: ```css #widget { width: 1fr; min-width: 30; max-width: 80; } ``` ## Spacing and Alignment ### Padding Space inside widget: ```css #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: ```css #widget { margin: 1; margin: 0 2; /* No vertical, 2 horizontal */ margin-left: 1; } ``` ### Alignment Position within container: ```css 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```css * { border: solid red; /* Temporary debugging */ } Container { border: solid blue; } Widget { border: solid green; } ``` Use Textual devtools: ```bash textual run --dev app.py ``` Add debug info to widgets: ```python def compose(self) -> ComposeResult: yield Label(f"Size: {self.size}") yield Label(f"Region: {self.region}") ```