10 KiB
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}")