or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

content.mdcore-framework.mdevents.mdindex.mdstyling.mdtesting.mdwidgets.md

styling.mddocs/

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

```