0
# Styling and Layout
1
2
CSS-like styling system with layout algorithms, reactive properties for automatic UI updates, color management, and geometric utilities for precise UI positioning and sizing.
3
4
## Capabilities
5
6
### CSS Styling System
7
8
Textual's CSS-like styling system allows you to style widgets using familiar CSS properties and selectors.
9
10
```python { .api }
11
class Styles:
12
"""Container for CSS style properties."""
13
14
def __init__(self):
15
"""Initialize styles container."""
16
17
def __setattr__(self, name: str, value: Any) -> None:
18
"""Set a style property."""
19
20
def __getattr__(self, name: str) -> Any:
21
"""Get a style property value."""
22
23
def refresh(self) -> None:
24
"""Refresh the styles."""
25
26
# Common style properties (partial list)
27
width: int | str # Widget width
28
height: int | str # Widget height
29
min_width: int # Minimum width
30
min_height: int # Minimum height
31
max_width: int # Maximum width
32
max_height: int # Maximum height
33
margin: tuple[int, int, int, int] # Margin (top, right, bottom, left)
34
padding: tuple[int, int, int, int] # Padding
35
border: tuple[str, Color] # Border style and color
36
background: Color # Background color
37
color: Color # Text color
38
text_align: str # Text alignment ("left", "center", "right")
39
opacity: float # Transparency (0.0 to 1.0)
40
display: str # Display type ("block", "none")
41
visibility: str # Visibility ("visible", "hidden")
42
43
class StyleSheet:
44
"""CSS stylesheet management."""
45
46
def __init__(self):
47
"""Initialize stylesheet."""
48
49
def add_source(self, css: str, *, path: str | None = None) -> None:
50
"""
51
Add CSS source to the stylesheet.
52
53
Parameters:
54
- css: CSS text content
55
- path: Optional file path for debugging
56
"""
57
58
def parse(self, css: str) -> None:
59
"""Parse CSS text and add rules."""
60
61
def read(self, path: str | Path) -> None:
62
"""Read CSS from a file."""
63
64
class CSSPathError(Exception):
65
"""Raised when CSS path is invalid."""
66
67
class DeclarationError(Exception):
68
"""Raised when CSS declaration is invalid."""
69
```
70
71
### Reactive System
72
73
Reactive properties automatically update the UI when values change, similar to modern web frameworks.
74
75
```python { .api }
76
class Reactive:
77
"""Reactive attribute descriptor for automatic UI updates."""
78
79
def __init__(
80
self,
81
default: Any = None,
82
*,
83
layout: bool = False,
84
repaint: bool = True,
85
init: bool = False,
86
always_update: bool = False,
87
compute: bool = True,
88
recompose: bool = False,
89
bindings: bool = False,
90
toggle_class: str | None = None
91
):
92
"""
93
Initialize reactive descriptor.
94
95
Parameters:
96
- default: Default value or callable that returns default
97
- layout: Whether changes trigger layout recalculation
98
- repaint: Whether changes trigger widget repaint
99
- init: Call watchers on initialization (post mount)
100
- always_update: Call watchers even when new value equals old
101
- compute: Run compute methods when attribute changes
102
- recompose: Compose the widget again when attribute changes
103
- bindings: Refresh bindings when reactive changes
104
- toggle_class: CSS class to toggle based on value truthiness
105
"""
106
107
def __get__(self, instance: Any, owner: type) -> Any:
108
"""Get the reactive value."""
109
110
def __set__(self, instance: Any, value: Any) -> None:
111
"""Set the reactive value and trigger updates."""
112
113
class ComputedProperty:
114
"""A computed reactive property."""
115
116
def __init__(self, function: Callable):
117
"""
118
Initialize computed property.
119
120
Parameters:
121
- function: Function to compute the value
122
"""
123
124
def watch(attribute: str):
125
"""
126
Decorator for watching reactive attribute changes.
127
128
Parameters:
129
- attribute: Name of reactive attribute to watch
130
131
Usage:
132
@watch("count")
133
def count_changed(self, old_value, new_value):
134
pass
135
"""
136
```
137
138
### Color System
139
140
Comprehensive color management with support for various color formats and ANSI terminal colors.
141
142
```python { .api }
143
class Color:
144
"""RGB color with alpha channel and ANSI support."""
145
146
def __init__(self, r: int, g: int, b: int, a: float = 1.0):
147
"""
148
Initialize a color.
149
150
Parameters:
151
- r: Red component (0-255)
152
- g: Green component (0-255)
153
- b: Blue component (0-255)
154
- a: Alpha channel (0.0-1.0)
155
"""
156
157
@classmethod
158
def parse(cls, color_text: str) -> Color:
159
"""
160
Parse color from text.
161
162
Parameters:
163
- color_text: Color specification (hex, name, rgb(), etc.)
164
165
Returns:
166
Parsed Color instance
167
"""
168
169
@classmethod
170
def from_hsl(cls, h: float, s: float, l: float, a: float = 1.0) -> Color:
171
"""
172
Create color from HSL values.
173
174
Parameters:
175
- h: Hue (0.0-360.0)
176
- s: Saturation (0.0-1.0)
177
- l: Lightness (0.0-1.0)
178
- a: Alpha (0.0-1.0)
179
"""
180
181
def __str__(self) -> str:
182
"""Get color as CSS-style string."""
183
184
def with_alpha(self, alpha: float) -> Color:
185
"""
186
Create new color with different alpha.
187
188
Parameters:
189
- alpha: New alpha value
190
191
Returns:
192
New Color instance
193
"""
194
195
# Properties
196
r: int # Red component
197
g: int # Green component
198
b: int # Blue component
199
a: float # Alpha channel
200
hex: str # Hex representation
201
rgb: tuple[int, int, int] # RGB tuple
202
rgba: tuple[int, int, int, float] # RGBA tuple
203
204
class HSL:
205
"""HSL color representation."""
206
207
def __init__(self, h: float, s: float, l: float, a: float = 1.0):
208
"""
209
Initialize HSL color.
210
211
Parameters:
212
- h: Hue (0.0-360.0)
213
- s: Saturation (0.0-1.0)
214
- l: Lightness (0.0-1.0)
215
- a: Alpha (0.0-1.0)
216
"""
217
218
# Properties
219
h: float # Hue
220
s: float # Saturation
221
l: float # Lightness
222
a: float # Alpha
223
224
class Gradient:
225
"""Color gradient definition."""
226
227
def __init__(self, *stops: tuple[float, Color]):
228
"""
229
Initialize gradient with color stops.
230
231
Parameters:
232
- *stops: Color stops as (position, color) tuples
233
"""
234
235
def get_color(self, position: float) -> Color:
236
"""
237
Get interpolated color at position.
238
239
Parameters:
240
- position: Position in gradient (0.0-1.0)
241
242
Returns:
243
Interpolated color
244
"""
245
246
class ColorParseError(Exception):
247
"""Raised when color parsing fails."""
248
```
249
250
### Geometric Utilities
251
252
Types and utilities for managing widget positioning, sizing, and layout calculations.
253
254
```python { .api }
255
class Offset:
256
"""X,Y coordinate pair."""
257
258
def __init__(self, x: int, y: int):
259
"""
260
Initialize offset.
261
262
Parameters:
263
- x: Horizontal offset
264
- y: Vertical offset
265
"""
266
267
def __add__(self, other: Offset) -> Offset:
268
"""Add two offsets."""
269
270
def __sub__(self, other: Offset) -> Offset:
271
"""Subtract two offsets."""
272
273
# Properties
274
x: int # Horizontal coordinate
275
y: int # Vertical coordinate
276
277
class Size:
278
"""Width and height dimensions."""
279
280
def __init__(self, width: int, height: int):
281
"""
282
Initialize size.
283
284
Parameters:
285
- width: Width in characters/cells
286
- height: Height in characters/cells
287
"""
288
289
def __add__(self, other: Size) -> Size:
290
"""Add two sizes."""
291
292
def __sub__(self, other: Size) -> Size:
293
"""Subtract two sizes."""
294
295
@property
296
def area(self) -> int:
297
"""Get total area (width * height)."""
298
299
# Properties
300
width: int # Width dimension
301
height: int # Height dimension
302
303
class Region:
304
"""Rectangular area with offset and size."""
305
306
def __init__(self, x: int, y: int, width: int, height: int):
307
"""
308
Initialize region.
309
310
Parameters:
311
- x: Left edge X coordinate
312
- y: Top edge Y coordinate
313
- width: Region width
314
- height: Region height
315
"""
316
317
@classmethod
318
def from_corners(cls, x1: int, y1: int, x2: int, y2: int) -> Region:
319
"""
320
Create region from corner coordinates.
321
322
Parameters:
323
- x1, y1: Top-left corner
324
- x2, y2: Bottom-right corner
325
"""
326
327
def contains(self, x: int, y: int) -> bool:
328
"""
329
Check if point is within region.
330
331
Parameters:
332
- x: Point X coordinate
333
- y: Point Y coordinate
334
335
Returns:
336
True if point is inside region
337
"""
338
339
def intersect(self, other: Region) -> Region:
340
"""
341
Get intersection with another region.
342
343
Parameters:
344
- other: Region to intersect with
345
346
Returns:
347
Intersection region
348
"""
349
350
def union(self, other: Region) -> Region:
351
"""
352
Get union with another region.
353
354
Parameters:
355
- other: Region to union with
356
357
Returns:
358
Union region
359
"""
360
361
# Properties
362
x: int # Left edge
363
y: int # Top edge
364
width: int # Width
365
height: int # Height
366
size: Size # Size as Size object
367
offset: Offset # Offset as Offset object
368
area: int # Total area
369
370
class Spacing:
371
"""Padding/margin spacing values."""
372
373
def __init__(self, top: int, right: int, bottom: int, left: int):
374
"""
375
Initialize spacing.
376
377
Parameters:
378
- top: Top spacing
379
- right: Right spacing
380
- bottom: Bottom spacing
381
- left: Left spacing
382
"""
383
384
@classmethod
385
def all(cls, value: int) -> Spacing:
386
"""Create uniform spacing."""
387
388
@classmethod
389
def horizontal(cls, value: int) -> Spacing:
390
"""Create horizontal-only spacing."""
391
392
@classmethod
393
def vertical(cls, value: int) -> Spacing:
394
"""Create vertical-only spacing."""
395
396
# Properties
397
top: int
398
right: int
399
bottom: int
400
left: int
401
horizontal: int # Combined left + right
402
vertical: int # Combined top + bottom
403
404
def clamp(value: float, minimum: float, maximum: float) -> float:
405
"""
406
Clamp value within range.
407
408
Parameters:
409
- value: Value to clamp
410
- minimum: Minimum allowed value
411
- maximum: Maximum allowed value
412
413
Returns:
414
Clamped value
415
"""
416
```
417
418
### Layout System
419
420
Layout algorithms for arranging widgets within containers.
421
422
```python { .api }
423
class Layout:
424
"""Base class for layout algorithms."""
425
426
def __init__(self):
427
"""Initialize layout."""
428
429
def arrange(
430
self,
431
parent: Widget,
432
children: list[Widget],
433
size: Size
434
) -> dict[Widget, Region]:
435
"""
436
Arrange child widgets within parent.
437
438
Parameters:
439
- parent: Parent container widget
440
- children: List of child widgets to arrange
441
- size: Available size for arrangement
442
443
Returns:
444
Mapping of widgets to their regions
445
"""
446
447
class VerticalLayout(Layout):
448
"""Stack widgets vertically."""
449
pass
450
451
class HorizontalLayout(Layout):
452
"""Arrange widgets horizontally."""
453
pass
454
455
class GridLayout(Layout):
456
"""CSS Grid-like layout system."""
457
458
def __init__(self, *, columns: int = 1, rows: int = 1):
459
"""
460
Initialize grid layout.
461
462
Parameters:
463
- columns: Number of grid columns
464
- rows: Number of grid rows
465
"""
466
```
467
468
## Usage Examples
469
470
### CSS Styling
471
472
```python
473
from textual.app import App
474
from textual.widgets import Static
475
from textual.color import Color
476
477
class StyledApp(App):
478
# External CSS file
479
CSS_PATH = "app.css"
480
481
def compose(self):
482
yield Static("Styled content", classes="fancy-box")
483
484
def on_mount(self):
485
# Programmatic styling
486
static = self.query_one(Static)
487
static.styles.background = Color.parse("blue")
488
static.styles.color = Color.parse("white")
489
static.styles.padding = (1, 2)
490
static.styles.border = ("solid", Color.parse("yellow"))
491
492
# app.css content:
493
"""
494
.fancy-box {
495
width: 50%;
496
height: 10;
497
text-align: center;
498
margin: 2;
499
border: thick $primary;
500
background: $surface;
501
}
502
503
Static:hover {
504
background: $primary 50%;
505
}
506
"""
507
```
508
509
### Reactive Properties
510
511
```python
512
from textual.app import App
513
from textual.widget import Widget
514
from textual.reactive import reactive, watch
515
from textual.widgets import Button, Static
516
517
class Counter(Widget):
518
"""A counter widget with reactive properties."""
519
520
# Reactive attribute that triggers repaint when changed
521
count = reactive(0)
522
523
def compose(self):
524
yield Static(f"Count: {self.count}", id="display")
525
yield Button("+", id="increment")
526
yield Button("-", id="decrement")
527
528
@watch("count")
529
def count_changed(self, old_value: int, new_value: int):
530
"""Called when count changes."""
531
display = self.query_one("#display", Static)
532
display.update(f"Count: {new_value}")
533
534
def on_button_pressed(self, event: Button.Pressed):
535
if event.button.id == "increment":
536
self.count += 1
537
elif event.button.id == "decrement":
538
self.count -= 1
539
540
class ReactiveApp(App):
541
def compose(self):
542
yield Counter()
543
```
544
545
### Color Management
546
547
```python
548
from textual.app import App
549
from textual.widgets import Static
550
from textual.color import Color, HSL, Gradient
551
552
class ColorApp(App):
553
def compose(self):
554
yield Static("Red text", id="red")
555
yield Static("HSL color", id="hsl")
556
yield Static("Parsed color", id="parsed")
557
558
def on_mount(self):
559
# Direct RGB color
560
red_widget = self.query_one("#red")
561
red_widget.styles.color = Color(255, 0, 0)
562
563
# HSL color conversion
564
hsl_widget = self.query_one("#hsl")
565
hsl_color = Color.from_hsl(240, 1.0, 0.5) # Blue
566
hsl_widget.styles.color = hsl_color
567
568
# Parse color from string
569
parsed_widget = self.query_one("#parsed")
570
parsed_widget.styles.color = Color.parse("#00ff00") # Green
571
572
# Gradient background (if supported)
573
gradient = Gradient(
574
(0.0, Color.parse("red")),
575
(0.5, Color.parse("yellow")),
576
(1.0, Color.parse("blue"))
577
)
578
```
579
580
### Geometric Calculations
581
582
```python
583
from textual.app import App
584
from textual.widget import Widget
585
from textual.geometry import Offset, Size, Region
586
from textual.events import MouseDown
587
588
class GeometryWidget(Widget):
589
"""Widget demonstrating geometric utilities."""
590
591
def __init__(self):
592
super().__init__()
593
self.center_region = Region(10, 5, 20, 10)
594
595
def on_mouse_down(self, event: MouseDown):
596
"""Handle mouse clicks with geometric calculations."""
597
click_point = Offset(event.x, event.y)
598
599
# Check if click is in center region
600
if self.center_region.contains(event.x, event.y):
601
self.log("Clicked in center region!")
602
603
# Calculate distance from center
604
center = Offset(
605
self.center_region.x + self.center_region.width // 2,
606
self.center_region.y + self.center_region.height // 2
607
)
608
distance_offset = click_point - center
609
distance = (distance_offset.x ** 2 + distance_offset.y ** 2) ** 0.5
610
611
self.log(f"Distance from center: {distance:.1f}")
612
613
class GeometryApp(App):
614
def compose(self):
615
yield GeometryWidget()
616
```
617
618
### Layout Customization
619
620
```python
621
from textual.app import App
622
from textual.containers import Container
623
from textual.widgets import Static
624
from textual.layouts import GridLayout
625
626
class LayoutApp(App):
627
def compose(self):
628
# Grid layout container
629
with Container():
630
container = self.query_one(Container)
631
container.styles.layout = GridLayout(columns=2, rows=2)
632
633
yield Static("Top Left", classes="grid-item")
634
yield Static("Top Right", classes="grid-item")
635
yield Static("Bottom Left", classes="grid-item")
636
yield Static("Bottom Right", classes="grid-item")
637
638
# CSS for grid items
639
"""
640
.grid-item {
641
height: 5;
642
border: solid white;
643
text-align: center;
644
content-align: center middle;
645
}
646
"""
647
```