CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-textual

Modern Text User Interface framework for building cross-platform terminal and web applications with Python

Overall
score

93%

Overview
Eval results
Files

styling.mddocs/

Styling and Layout

CSS-like styling system with layout algorithms, reactive properties for automatic UI updates, color management, and geometric utilities for precise UI positioning and sizing.

Capabilities

CSS Styling System

Textual's CSS-like styling system allows you to style widgets using familiar CSS properties and selectors.

class Styles:
    """Container for CSS style properties."""
    
    def __init__(self):
        """Initialize styles container."""
    
    def __setattr__(self, name: str, value: Any) -> None:
        """Set a style property."""
    
    def __getattr__(self, name: str) -> Any:
        """Get a style property value."""
    
    def refresh(self) -> None:
        """Refresh the styles."""
    
    # Common style properties (partial list)
    width: int | str  # Widget width
    height: int | str  # Widget height
    min_width: int  # Minimum width
    min_height: int  # Minimum height
    max_width: int  # Maximum width  
    max_height: int  # Maximum height
    margin: tuple[int, int, int, int]  # Margin (top, right, bottom, left)
    padding: tuple[int, int, int, int]  # Padding
    border: tuple[str, Color]  # Border style and color
    background: Color  # Background color
    color: Color  # Text color
    text_align: str  # Text alignment ("left", "center", "right")
    opacity: float  # Transparency (0.0 to 1.0)
    display: str  # Display type ("block", "none")
    visibility: str  # Visibility ("visible", "hidden")

class StyleSheet:
    """CSS stylesheet management."""
    
    def __init__(self):
        """Initialize stylesheet."""
    
    def add_source(self, css: str, *, path: str | None = None) -> None:
        """
        Add CSS source to the stylesheet.
        
        Parameters:
        - css: CSS text content
        - path: Optional file path for debugging
        """
    
    def parse(self, css: str) -> None:
        """Parse CSS text and add rules."""
    
    def read(self, path: str | Path) -> None:
        """Read CSS from a file."""

class CSSPathError(Exception):
    """Raised when CSS path is invalid."""

class DeclarationError(Exception):
    """Raised when CSS declaration is invalid."""

Reactive System

Reactive properties automatically update the UI when values change, similar to modern web frameworks.

class Reactive:
    """Reactive attribute descriptor for automatic UI updates."""
    
    def __init__(
        self,
        default: Any = None,
        *,
        layout: bool = False,
        repaint: bool = True,
        init: bool = False,
        always_update: bool = False,
        compute: bool = True,
        recompose: bool = False,
        bindings: bool = False,
        toggle_class: str | None = None
    ):
        """
        Initialize reactive descriptor.
        
        Parameters:
        - default: Default value or callable that returns default
        - layout: Whether changes trigger layout recalculation
        - repaint: Whether changes trigger widget repaint
        - init: Call watchers on initialization (post mount)
        - always_update: Call watchers even when new value equals old
        - compute: Run compute methods when attribute changes
        - recompose: Compose the widget again when attribute changes
        - bindings: Refresh bindings when reactive changes
        - toggle_class: CSS class to toggle based on value truthiness
        """
    
    def __get__(self, instance: Any, owner: type) -> Any:
        """Get the reactive value."""
    
    def __set__(self, instance: Any, value: Any) -> None:
        """Set the reactive value and trigger updates."""

class ComputedProperty:
    """A computed reactive property."""
    
    def __init__(self, function: Callable):
        """
        Initialize computed property.
        
        Parameters:
        - function: Function to compute the value
        """

def watch(attribute: str):
    """
    Decorator for watching reactive attribute changes.
    
    Parameters:
    - attribute: Name of reactive attribute to watch
    
    Usage:
    @watch("count")
    def count_changed(self, old_value, new_value):
        pass
    """

Color System

Comprehensive color management with support for various color formats and ANSI terminal colors.

class Color:
    """RGB color with alpha channel and ANSI support."""
    
    def __init__(self, r: int, g: int, b: int, a: float = 1.0):
        """
        Initialize a color.
        
        Parameters:
        - r: Red component (0-255)
        - g: Green component (0-255)  
        - b: Blue component (0-255)
        - a: Alpha channel (0.0-1.0)
        """
    
    @classmethod
    def parse(cls, color_text: str) -> Color:
        """
        Parse color from text.
        
        Parameters:
        - color_text: Color specification (hex, name, rgb(), etc.)
        
        Returns:  
        Parsed Color instance
        """
    
    @classmethod
    def from_hsl(cls, h: float, s: float, l: float, a: float = 1.0) -> Color:
        """
        Create color from HSL values.
        
        Parameters:
        - h: Hue (0.0-360.0)
        - s: Saturation (0.0-1.0)
        - l: Lightness (0.0-1.0)
        - a: Alpha (0.0-1.0)
        """
    
    def __str__(self) -> str:
        """Get color as CSS-style string."""
    
    def with_alpha(self, alpha: float) -> Color:
        """
        Create new color with different alpha.
        
        Parameters:
        - alpha: New alpha value
        
        Returns:
        New Color instance
        """
    
    # Properties
    r: int  # Red component
    g: int  # Green component  
    b: int  # Blue component
    a: float  # Alpha channel
    hex: str  # Hex representation
    rgb: tuple[int, int, int]  # RGB tuple
    rgba: tuple[int, int, int, float]  # RGBA tuple

class HSL:
    """HSL color representation."""
    
    def __init__(self, h: float, s: float, l: float, a: float = 1.0):
        """
        Initialize HSL color.
        
        Parameters:
        - h: Hue (0.0-360.0)
        - s: Saturation (0.0-1.0)
        - l: Lightness (0.0-1.0) 
        - a: Alpha (0.0-1.0)
        """
    
    # Properties
    h: float  # Hue
    s: float  # Saturation
    l: float  # Lightness
    a: float  # Alpha

class Gradient:
    """Color gradient definition."""
    
    def __init__(self, *stops: tuple[float, Color]):
        """
        Initialize gradient with color stops.
        
        Parameters:
        - *stops: Color stops as (position, color) tuples
        """
    
    def get_color(self, position: float) -> Color:
        """
        Get interpolated color at position.
        
        Parameters:
        - position: Position in gradient (0.0-1.0)
        
        Returns:
        Interpolated color
        """

class ColorParseError(Exception):
    """Raised when color parsing fails."""

Geometric Utilities

Types and utilities for managing widget positioning, sizing, and layout calculations.

class Offset:
    """X,Y coordinate pair."""
    
    def __init__(self, x: int, y: int):
        """
        Initialize offset.
        
        Parameters:
        - x: Horizontal offset
        - y: Vertical offset
        """
    
    def __add__(self, other: Offset) -> Offset:
        """Add two offsets."""
    
    def __sub__(self, other: Offset) -> Offset:
        """Subtract two offsets."""
    
    # Properties
    x: int  # Horizontal coordinate
    y: int  # Vertical coordinate

class Size:
    """Width and height dimensions."""
    
    def __init__(self, width: int, height: int):
        """
        Initialize size.
        
        Parameters:
        - width: Width in characters/cells
        - height: Height in characters/cells
        """
    
    def __add__(self, other: Size) -> Size:
        """Add two sizes."""
    
    def __sub__(self, other: Size) -> Size:
        """Subtract two sizes."""
    
    @property
    def area(self) -> int:
        """Get total area (width * height)."""
    
    # Properties
    width: int  # Width dimension
    height: int  # Height dimension

class Region:
    """Rectangular area with offset and size."""
    
    def __init__(self, x: int, y: int, width: int, height: int):
        """
        Initialize region.
        
        Parameters:
        - x: Left edge X coordinate
        - y: Top edge Y coordinate
        - width: Region width
        - height: Region height
        """
    
    @classmethod
    def from_corners(cls, x1: int, y1: int, x2: int, y2: int) -> Region:
        """
        Create region from corner coordinates.
        
        Parameters:
        - x1, y1: Top-left corner
        - x2, y2: Bottom-right corner
        """
    
    def contains(self, x: int, y: int) -> bool:
        """
        Check if point is within region.
        
        Parameters:
        - x: Point X coordinate
        - y: Point Y coordinate
        
        Returns:
        True if point is inside region
        """
    
    def intersect(self, other: Region) -> Region:
        """
        Get intersection with another region.
        
        Parameters:
        - other: Region to intersect with
        
        Returns:
        Intersection region
        """
    
    def union(self, other: Region) -> Region:
        """
        Get union with another region.
        
        Parameters:
        - other: Region to union with
        
        Returns:
        Union region
        """
    
    # Properties
    x: int  # Left edge
    y: int  # Top edge
    width: int  # Width
    height: int  # Height
    size: Size  # Size as Size object
    offset: Offset  # Offset as Offset object
    area: int  # Total area

class Spacing:
    """Padding/margin spacing values."""
    
    def __init__(self, top: int, right: int, bottom: int, left: int):
        """
        Initialize spacing.
        
        Parameters:
        - top: Top spacing
        - right: Right spacing
        - bottom: Bottom spacing
        - left: Left spacing
        """
    
    @classmethod
    def all(cls, value: int) -> Spacing:
        """Create uniform spacing."""
    
    @classmethod
    def horizontal(cls, value: int) -> Spacing:
        """Create horizontal-only spacing."""
    
    @classmethod
    def vertical(cls, value: int) -> Spacing:
        """Create vertical-only spacing."""
    
    # Properties
    top: int
    right: int
    bottom: int
    left: int
    horizontal: int  # Combined left + right
    vertical: int  # Combined top + bottom

def clamp(value: float, minimum: float, maximum: float) -> float:
    """
    Clamp value within range.
    
    Parameters:
    - value: Value to clamp
    - minimum: Minimum allowed value
    - maximum: Maximum allowed value
    
    Returns:
    Clamped value
    """

Layout System

Layout algorithms for arranging widgets within containers.

class Layout:
    """Base class for layout algorithms."""
    
    def __init__(self):
        """Initialize layout."""
    
    def arrange(
        self,
        parent: Widget,
        children: list[Widget],
        size: Size
    ) -> dict[Widget, Region]:
        """
        Arrange child widgets within parent.
        
        Parameters:
        - parent: Parent container widget
        - children: List of child widgets to arrange
        - size: Available size for arrangement
        
        Returns:
        Mapping of widgets to their regions
        """

class VerticalLayout(Layout):
    """Stack widgets vertically."""
    pass

class HorizontalLayout(Layout):
    """Arrange widgets horizontally.""" 
    pass

class GridLayout(Layout):
    """CSS Grid-like layout system."""
    
    def __init__(self, *, columns: int = 1, rows: int = 1):
        """
        Initialize grid layout.
        
        Parameters:
        - columns: Number of grid columns
        - rows: Number of grid rows
        """

Usage Examples

CSS Styling

from textual.app import App
from textual.widgets import Static
from textual.color import Color

class StyledApp(App):
    # External CSS file
    CSS_PATH = "app.css"
    
    def compose(self):
        yield Static("Styled content", classes="fancy-box")
    
    def on_mount(self):
        # Programmatic styling
        static = self.query_one(Static)
        static.styles.background = Color.parse("blue")
        static.styles.color = Color.parse("white")
        static.styles.padding = (1, 2)
        static.styles.border = ("solid", Color.parse("yellow"))

# app.css content:
"""
.fancy-box {
    width: 50%;
    height: 10;
    text-align: center;
    margin: 2;
    border: thick $primary;
    background: $surface;
}

Static:hover {
    background: $primary 50%;
}
"""

Reactive Properties

from textual.app import App
from textual.widget import Widget
from textual.reactive import reactive, watch
from textual.widgets import Button, Static

class Counter(Widget):
    """A counter widget with reactive properties."""
    
    # Reactive attribute that triggers repaint when changed
    count = reactive(0)
    
    def compose(self):
        yield Static(f"Count: {self.count}", id="display")
        yield Button("+", id="increment")
        yield Button("-", id="decrement")
    
    @watch("count")
    def count_changed(self, old_value: int, new_value: int):
        """Called when count changes."""
        display = self.query_one("#display", Static)
        display.update(f"Count: {new_value}")
    
    def on_button_pressed(self, event: Button.Pressed):
        if event.button.id == "increment":
            self.count += 1
        elif event.button.id == "decrement":
            self.count -= 1

class ReactiveApp(App):
    def compose(self):
        yield Counter()

Color Management

from textual.app import App
from textual.widgets import Static
from textual.color import Color, HSL, Gradient

class ColorApp(App):
    def compose(self):
        yield Static("Red text", id="red")
        yield Static("HSL color", id="hsl")
        yield Static("Parsed color", id="parsed")
    
    def on_mount(self):
        # Direct RGB color
        red_widget = self.query_one("#red")
        red_widget.styles.color = Color(255, 0, 0)
        
        # HSL color conversion
        hsl_widget = self.query_one("#hsl")
        hsl_color = Color.from_hsl(240, 1.0, 0.5)  # Blue
        hsl_widget.styles.color = hsl_color
        
        # Parse color from string
        parsed_widget = self.query_one("#parsed")
        parsed_widget.styles.color = Color.parse("#00ff00")  # Green
        
        # Gradient background (if supported)
        gradient = Gradient(
            (0.0, Color.parse("red")),
            (0.5, Color.parse("yellow")), 
            (1.0, Color.parse("blue"))
        )

Geometric Calculations

from textual.app import App
from textual.widget import Widget
from textual.geometry import Offset, Size, Region
from textual.events import MouseDown

class GeometryWidget(Widget):
    """Widget demonstrating geometric utilities."""
    
    def __init__(self):
        super().__init__()
        self.center_region = Region(10, 5, 20, 10)
    
    def on_mouse_down(self, event: MouseDown):
        """Handle mouse clicks with geometric calculations."""
        click_point = Offset(event.x, event.y)
        
        # Check if click is in center region
        if self.center_region.contains(event.x, event.y):
            self.log("Clicked in center region!")
        
        # Calculate distance from center
        center = Offset(
            self.center_region.x + self.center_region.width // 2,
            self.center_region.y + self.center_region.height // 2
        )
        distance_offset = click_point - center
        distance = (distance_offset.x ** 2 + distance_offset.y ** 2) ** 0.5
        
        self.log(f"Distance from center: {distance:.1f}")

class GeometryApp(App):
    def compose(self):
        yield GeometryWidget()

Layout Customization

from textual.app import App
from textual.containers import Container
from textual.widgets import Static
from textual.layouts import GridLayout

class LayoutApp(App):
    def compose(self):
        # Grid layout container
        with Container():
            container = self.query_one(Container)
            container.styles.layout = GridLayout(columns=2, rows=2)
            
            yield Static("Top Left", classes="grid-item")
            yield Static("Top Right", classes="grid-item")
            yield Static("Bottom Left", classes="grid-item")
            yield Static("Bottom Right", classes="grid-item")

# CSS for grid items
"""
.grid-item {
    height: 5;
    border: solid white;
    text-align: center;
    content-align: center middle;
}
"""

Install with Tessl CLI

npx tessl i tessl/pypi-textual

docs

content.md

core-framework.md

events.md

index.md

styling.md

testing.md

widgets.md

tile.json