11 KiB
11 KiB
Textual CSS Styling Guide
Complete guide to styling Textual applications with TCSS (Textual CSS).
CSS Basics
Inline Styles
Define styles directly in widget class:
class MyWidget(Widget):
DEFAULT_CSS = """
MyWidget {
background: $primary;
color: $text;
border: solid $accent;
}
"""
External Stylesheets
Load from file:
class MyApp(App):
CSS_PATH = "app.tcss" # Load from app.tcss file
Multiple Stylesheets
Load multiple files:
class MyApp(App):
CSS_PATH = ["base.tcss", "theme.tcss", "overrides.tcss"]
Selectors
Type Selectors
Target widget types:
Button {
background: blue;
}
Label {
color: white;
}
ID Selectors
Target specific widgets:
#submit-button {
background: green;
}
#error-message {
color: red;
}
Class Selectors
Target classes:
.highlight {
background: yellow;
color: black;
}
.card {
border: solid white;
padding: 1;
}
Pseudo-classes
Target widget states:
Button:hover {
background: lighten($primary, 20%);
}
Button:focus {
border: thick $accent;
}
Input:focus {
border: solid $success;
}
/* Disabled state */
Button:disabled {
opacity: 50%;
}
Descendant Selectors
Target nested widgets:
/* Any Label inside a Container */
Container Label {
color: gray;
}
/* Direct children only */
Container > Label {
color: white;
}
/* Specific nesting */
#sidebar .menu-item {
padding: 1;
}
Multiple Selectors
Apply same style to multiple targets:
Button, Input, Select {
border: solid $accent;
}
.error, .warning {
font-weight: bold;
}
Colors
Semantic Colors
Use theme colors:
Widget {
background: $background;
color: $text;
border: solid $primary;
}
/* Available semantic colors */
$primary /* Primary theme color */
$secondary /* Secondary theme color */
$accent /* Accent color */
$background /* Background color */
$surface /* Surface color */
$panel /* Panel color */
$text /* Primary text color */
$text-muted /* Muted text */
$text-disabled /* Disabled text */
$success /* Success state */
$warning /* Warning state */
$error /* Error state */
$boost /* Highlight color */
Color Formats
Define custom colors:
Widget {
background: #1e3a8a; /* Hex */
color: rgb(255, 255, 255); /* RGB */
border-color: rgba(255, 0, 0, 0.5); /* RGBA with alpha */
}
/* Named colors */
Widget {
background: transparent;
color: black;
border-color: white;
}
Color Functions
Manipulate colors:
Widget {
background: darken($primary, 20%);
color: lighten($text, 10%);
border-color: fade($accent, 50%);
}
Typography
Text Style
Label {
text-style: bold; /* bold, italic, underline */
text-style: bold italic; /* Multiple styles */
text-style: reverse; /* Reverse colors */
text-style: strike; /* Strikethrough */
}
Text Alignment
Label {
text-align: left; /* left, center, right, justify */
}
Text Opacity
Label {
text-opacity: 70%; /* Semi-transparent text */
}
Borders
Border Styles
Widget {
border: solid $accent; /* Solid border */
border: dashed blue; /* Dashed */
border: heavy green; /* Heavy */
border: double white; /* Double */
border: thick $primary; /* Thick */
border: none; /* No border */
}
Border Sides
Widget {
border-top: solid red;
border-right: dashed blue;
border-bottom: thick green;
border-left: double white;
}
Border Title
Widget {
border: solid $accent;
border-title-align: center; /* left, center, right */
}
Dimensions
Width
Widget {
width: 40; /* Fixed columns */
width: 50%; /* Percentage of parent */
width: 1fr; /* Fractional unit */
width: auto; /* Size to content */
}
Height
Widget {
height: 20; /* Fixed rows */
height: 100%; /* Full parent height */
height: auto; /* Size to content */
}
Min/Max Constraints
Widget {
min-width: 20;
max-width: 80;
min-height: 10;
max-height: 50;
}
Spacing
Padding
Space inside widget:
Widget {
padding: 1; /* All sides */
padding: 1 2; /* Vertical Horizontal */
padding: 1 2 3 4; /* Top Right Bottom Left */
}
/* Individual sides */
Widget {
padding-top: 1;
padding-right: 2;
padding-bottom: 1;
padding-left: 2;
}
Margin
Space outside widget:
Widget {
margin: 1;
margin: 0 2;
margin: 1 2 1 2;
}
/* Individual sides */
Widget {
margin-top: 1;
margin-right: 2;
}
Layout Properties
Display
Control visibility:
Widget {
display: block; /* Visible */
display: none; /* Hidden */
}
Visibility
Alternative to display:
Widget {
visibility: visible;
visibility: hidden; /* Hidden but takes space */
}
Opacity
Transparency:
Widget {
opacity: 100%; /* Fully opaque */
opacity: 50%; /* Semi-transparent */
opacity: 0%; /* Fully transparent */
}
Layout Type
Container {
layout: vertical; /* Stack vertically */
layout: horizontal; /* Stack horizontally */
layout: grid; /* Grid layout */
}
Grid Properties
Container {
layout: grid;
grid-size: 3 2; /* 3 columns, 2 rows */
grid-gutter: 1 2; /* Vertical Horizontal gaps */
grid-rows: 10 auto 1fr; /* Row sizes */
grid-columns: 1fr 2fr; /* Column sizes */
}
/* Grid item spanning */
Widget {
column-span: 2; /* Span 2 columns */
row-span: 3; /* Span 3 rows */
}
Alignment
Container {
align: center middle; /* Horizontal Vertical */
align-horizontal: left; /* left, center, right */
align-vertical: top; /* top, middle, bottom */
}
/* Content alignment */
Container {
content-align: center middle;
content-align-horizontal: right;
content-align-vertical: bottom;
}
Scrollbars
Widget {
overflow: auto; /* Show scrollbars when needed */
overflow: scroll; /* Always show scrollbars */
overflow: hidden; /* No scrollbars */
}
/* Individual axes */
Widget {
overflow-x: auto;
overflow-y: scroll;
}
/* Scrollbar styling */
Widget {
scrollbar-background: $panel;
scrollbar-color: $primary;
scrollbar-color-hover: $accent;
scrollbar-color-active: $boost;
}
Effects
Transitions
Animate property changes:
Button {
background: blue;
transition: background 300ms;
}
Button:hover {
background: lightblue; /* Animates over 300ms */
}
/* Multiple properties */
Widget {
transition: background 200ms, border 150ms;
}
Offset
Position adjustment:
Widget {
offset: 1 2; /* X Y offset */
offset-x: 1;
offset-y: 2;
}
Layer
Z-index equivalent:
Widget {
layer: above; /* Higher layer */
layer: below; /* Lower layer */
}
Docking
Pin widgets to edges:
#header {
dock: top;
height: 3;
}
#sidebar {
dock: left;
width: 30;
}
#footer {
dock: bottom;
height: 3;
}
Theme Variables
Define reusable values:
/* Define variables */
Screen {
--card-bg: #1e3a8a;
--card-border: white;
--card-padding: 1 2;
}
/* Use variables */
.card {
background: var(--card-bg);
border: solid var(--card-border);
padding: var(--card-padding);
}
Complete Theme Example
/* app.tcss */
/* Theme colors */
Screen {
background: #0f172a;
color: #e2e8f0;
}
/* Headers */
Header {
background: #1e293b;
color: #60a5fa;
dock: top;
height: 3;
}
Footer {
background: #1e293b;
color: #94a3b8;
dock: bottom;
height: 1;
}
/* Buttons */
Button {
background: #3b82f6;
color: white;
border: none;
margin: 0 1;
padding: 0 2;
min-width: 16;
transition: background 200ms;
}
Button:hover {
background: #60a5fa;
}
Button:focus {
border: solid #93c5fd;
}
Button.-primary {
background: #10b981;
}
Button.-primary:hover {
background: #34d399;
}
Button.-error {
background: #ef4444;
}
Button.-error:hover {
background: #f87171;
}
/* Inputs */
Input {
border: solid #475569;
background: #1e293b;
color: #e2e8f0;
padding: 0 1;
}
Input:focus {
border: solid #3b82f6;
}
/* Containers */
.card {
background: #1e293b;
border: solid #334155;
padding: 1 2;
margin: 1;
}
.card > .title {
text-style: bold;
color: #60a5fa;
margin-bottom: 1;
}
/* Data tables */
DataTable {
background: #1e293b;
}
DataTable > .datatable--header {
background: #334155;
color: #60a5fa;
text-style: bold;
}
DataTable > .datatable--cursor {
background: #3b82f6;
}
/* Scrollbars */
*::-webkit-scrollbar {
scrollbar-background: #1e293b;
scrollbar-color: #475569;
}
*::-webkit-scrollbar:hover {
scrollbar-color: #64748b;
}
Dark/Light Themes
Support theme switching:
class MyApp(App):
ENABLE_DARK_MODE = True
CSS = """
/* Dark theme (default) */
Screen {
background: #0f172a;
color: #e2e8f0;
}
/* Light theme */
Screen.light {
background: #f8fafc;
color: #1e293b;
}
"""
def action_toggle_theme(self) -> None:
self.dark = not self.dark
Responsive Styles
Conditional styles based on size:
/* Default (small screens) */
#sidebar {
width: 100%;
}
/* Medium screens */
Screen:width-gt-80 #sidebar {
width: 30;
}
/* Large screens */
Screen:width-gt-120 #sidebar {
width: 40;
}
Best Practices
- Use semantic colors - Prefer
$primaryover hardcoded values - Organize CSS - Group related styles together
- Use classes - Reusable styles via classes, not IDs
- Minimize specificity - Avoid overly specific selectors
- Use transitions - Smooth state changes
- Test both themes - Ensure dark/light compatibility
- Keep CSS DRY - Use variables for repeated values
- Document custom variables - Comment non-obvious choices
Debugging Styles
View computed styles:
def on_mount(self) -> None:
widget = self.query_one("#my-widget")
self.log(widget.styles) # Log all computed styles
Use Textual devtools:
textual run --dev app.py
# Press F1 to view CSS inspector
Temporary debugging borders:
* {
border: solid red; /* See all widget boundaries */
}