Styling
Style Struct
Style defines visual appearance. All fields are optional. Background color (bg) inherits from parent containers when unset; other fields use widget or theme defaults.
Style::new()
.fg(Color::Blue)
.bg(Color::indexed(235))
.bold()
.italic()
.underline()
.dim()
.reverse()| Method | Effect |
|---|---|
.fg(Color) | Foreground color |
.bg(Color) | Background color |
.bold() | Bold text |
.not_bold() | Explicitly disable bold (suppresses renderer fallbacks) |
.dim() | Dimmed/faint text |
.dim_by(f32) | Dim resolved fg/bg and cell backdrop |
.tint_by(Color, f32) | Tint existing rendered cells toward a color |
.lighten_by(f32) | Lighten resolved fg/bg colors |
.transform_fg(ColorTransform) | Transform the resolved foreground color |
.transform_bg(ColorTransform) | Transform the resolved background color |
.contrast_policy(ContrastPolicy) | Override contrast adjustment for this style |
.italic() | Italic text |
.underline() | Underline |
.reverse() | Swap fg/bg |
Style also exposes a tint field (Option<(Color, f32)>) for advanced/manual construction, but normal usage should prefer .tint_by(color, alpha).
Relative transforms are resolved after style patching, so they work well with theme-provided or inherited style values:
let disabled = Style::new().transform_fg(ColorTransform::Dim(0.5));
let warning_surface = Style::new().transform_bg(ColorTransform::Tint(Color::Yellow, 0.25));
let washed_out = Style::new().transform_fg(ColorTransform::Opacity(0.6));
let forced_readable = Style::new().contrast_policy(ContrastPolicy::Apca);Note:
Stylehas.lighten_by(...)but no.lighten(), and.tint_by(...)but no.tint()convenience method.
Style Inheritance
Only background color (bg) automatically inherits from parent containers. Foreground color (fg) and text modifiers (bold, italic, etc.) do not inherit - each widget resolves its own fg independently.
How Background Inheritance Works
When a container (VStack, HStack, Frame, etc.) has bg set, the framework fills its entire rectangular area with that background color before rendering children. Children that don't set their own bg naturally show the parent's background through the terminal buffer.
// ✅ GOOD: Set bg once on the parent container - children see it automatically
VStack::new()
.style(Style::new().bg(Color::indexed(235)))
.child(Text::new("Shows bg from VStack"))
.child(Text::new("Also shows bg from VStack"))
.child(Button::new("Also shows bg from VStack"))
// ❌ BAD: Setting bg on every single widget - all of these are redundant
VStack::new()
.style(Style::new().bg(Color::indexed(235)))
.child(Text::new("A").style(Style::new().bg(Color::indexed(235)))) // redundant!
.child(Text::new("B").style(Style::new().bg(Color::indexed(235)))) // redundant!
.child(Button::new("C").style(Style::new().bg(Color::indexed(235)))) // redundant!Note:
fgdoes NOT work this way. Each widget must set its ownfgif you want a specific text color. Setting.fg(Color::White)on a parent VStack does not make children's text white.
Sub-Style Inheritance
Don't repeat the parent's bg on every sub-style variant either - only set bg when you want a different background for that state:
// ❌ BAD: Repeating bg on every style variant
Input::new(query.clone())
.style(Style::new().fg(Color::White).bg(Color::indexed(235)))
.focus_style(Style::new().fg(Color::White).bg(Color::indexed(235)).bold())
// ✅ GOOD: bg is inherited from the parent container; only set fg and modifiers
Input::new(query.clone())
.style(Style::new().fg(Color::White))
.focus_style(Style::new().fg(Color::White).bold())Style Precedence
| Priority | Source |
|---|---|
| Highest | Explicit widget style (set directly on the widget) |
| ThemeProvider (applied to the subtree) | |
| Parent container bg (painted in the terminal buffer) | |
| Lowest | App-level theme default |
Colors
Color::Red // Named ANSI color
Color::indexed(235) // 256-color palette (u8)
Color::rgb(30, 40, 50) // True color
Color::hex("#1E2832") // Hex string (invalid input falls back to Color::Reset)
Color::Backdrop // Clear fg but preserve the background already underneath
Color::Transparent // Skip painting fg/bg - show whatever is already in the buffer / parentColor::Transparent is not a pigment: it tells the renderer not to set that style channel on ratatui cells, so lower layers stay visible. It differs from Color::Reset, which selects the terminal’s default palette for that attribute. In Style::patch, a transparent overlay leaves the resolved base color for that channel unchanged.
Color::Backdrop is intended for surface/background fills. It preserves the background color already in the buffer while still allowing the surface to clear text/foreground content above it. This matches the old modal behavior where the dialog body blanked underlying text without painting a new solid background.
Named colors: Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, Gray, DarkGray, LightRed, LightGreen, LightYellow, LightBlue, LightMagenta, LightCyan.
Tip: Prefer
Color::rgb(...)for interactive/selection styles when exact contrast matters. Named ANSI colors vary by terminal palette.
Palette (Tailwind-style colors)
tui_lipan::style::palette provides a comprehensive color palette based on Tailwind CSS. Use it for consistent, designer-friendly colors across your app.
Top-level 500-series constants (quick access):
SLATE, GRAY, ZINC, NEUTRAL, STONE, RED, ORANGE, AMBER, YELLOW, LIME, GREEN, EMERALD, TEAL, CYAN, SKY, BLUE, INDIGO, VIOLET, PURPLE, FUCHSIA, PINK, ROSE
Color family modules with shades B50–B950 (light to dark):
slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose
use tui_lipan::style::{palette, Style};
// Top-level 500-series
Style::new().fg(palette::BLUE)
// Shades (e.g. red::B500, slate::B200)
Style::new().fg(palette::red::B500).bg(palette::slate::B900)Color Transform Helpers
Use these for direct color manipulation:
| Method | Effect |
|---|---|
Color::dim() | Dim by default amount (0.35) |
Color::dim_by(f32) | Dim by explicit amount 0.0..=1.0 |
Color::lighten() | Lighten by default amount (0.35) |
Color::lighten_by(f32) | Lighten by explicit amount 0.0..=1.0 |
Color::blend_toward(Color, f32) | Blend toward target color by alpha |
let dialog_backdrop = Style::new().tint_by(Color::rgb(10, 20, 60), 0.55);
let boosted_text = Style::new().fg(Color::Blue.lighten());
let softer_text = Style::new().fg(Color::Blue.lighten_by(0.20));
let inherited_dim = Style::new().transform_fg(ColorTransform::Dim(0.5));
let inherited_opacity = Style::new().transform_fg(ColorTransform::Opacity(0.6));Note:
ColorTransform::Opacityblends the foreground toward the resolved cell background. It has no effect when the cell background isColor::Reset(the terminal default) because there is no RGB value to blend toward. To make opacity work through transparent/reset backgrounds, supply the terminal's default background color toApp::terminal_bg()- see the App configuration section.
Visual Effects
VisualEffect is a value-based post-processing model for EffectScope. Unlike Style, these effects do not describe widget-local text styling; they mutate the already-rendered cells inside an EffectScope rect.
EffectScope::new()
.effect(VisualEffect::PaletteQuantize {
palette: EffectPalette::Gameboy,
})
.effect(VisualEffect::Scanlines {
strength: 0.18,
spacing: 2,
})
.child(content)Common variants:
| Type | Purpose |
|---|---|
VisualEffect::ColorTransform | Apply relative color transforms (Dim, Lighten, Opacity, Tint) to fg/bg of each cell. Constructors: dim, lighten, tint, transform_fg, transform_bg |
VisualEffect::ContrastPolicy | Apply ContrastPolicy to ensure text legibility |
VisualEffect::Monochrome | Desaturation / grayscale conversion |
VisualEffect::PaletteQuantize | Reduce colors to a preset or custom palette |
VisualEffect::Scanlines | Static row-based dimming mask |
VisualEffect::RainbowWave | Animated color cycling by position and frame phase, blended back into the subtree |
VisualEffect::Gradient | Sine-eased mirrored ColorGradient wash sampled in scope-local coordinates; optional animation via speed / frame phase |
VisualEffect::RetroCrt | Retro preset built from palette, scanline, and flicker primitives |
VisualEffect::Clipped | Bounds and/or CellMask to restrict another effect - see widgets/effects.md |
Supporting enums:
| Enum | Variants |
|---|---|
EffectAxis | Horizontal, Vertical, Diagonal |
EffectPalette | Cga, Gameboy, Amber, Green, Custom(Vec<Color>) |
RetroPreset | Amber, Green, Cga, Gameboy, VaultTec |
Use Style when you need inherited colors, focus/hover patches, or per-widget presentation. Use VisualEffect when you want to transform the final composed output of an entire subtree.
For widgets like MouseRegion, these form two distinct layers:
hover_style(...)paints the hovered region before child content is rendered. It is best for hover backgrounds and modifiers; child text commonly paints its own foreground afterward, sohover_style(Style::new().fg(...))may not recolor that text.hover_effect(...)applies a visual post-processing transformation to the rendered child content. Use it when you need to change colors that children already painted, such as text foreground.hover_tint(color, alpha)is a symmetric tint shorthand: it blends both foreground and background towardcolor. Atalpha = 1.0, both channels becomecolor; usehover_effect(VisualEffect::transform_fg(ColorTransform::Tint(color, 1.0)))when you only want to recolor text.
Layout Primitives
Length
| Value | Meaning |
|---|---|
Length::Auto | Size to content |
Length::Px(u16) | Fixed cell count |
Length::Percent(u16) | Percentage of available space (clamped to 0..=100) |
Length::Flex(u16) | Proportional share of remaining space |
Containers (VStack, HStack) default to Flex(1) for both axes.
Layout Constraints
LayoutConstraints and Element::{min_width,min_height,max_width,max_height} use Length:
Px(n)is absolute.Percent(p)resolves against the parent-allocated size.Auto/Flex(_)mean no minimum (min) or no cap (max).- Percent constraints are ignored when the parent size is unknown during measurement.
Padding
// Uniform (all sides)
.padding(1) // 1 cell on all sides
// or: Padding::from(1u16)
// Vertical + Horizontal
.padding((2, 1)) // top/bottom=2, left/right=1
// or: Padding::from((2u16, 1u16))
// Full control (top, right, bottom, left)
.padding((1, 2, 1, 2))
// or: Padding::from((1u16, 2u16, 1u16, 2u16))Padding methods: .horizontal() → left+right sum, .vertical() → top+bottom sum.
Align
Cross-axis alignment for stacks and containers:
| Value | Effect |
|---|---|
Align::Start | Top/left (default) |
Align::Center | Centered |
Align::End | Bottom/right |
Align::Stretch | Fill available space |
Justify
Main-axis alignment for stacks:
| Value | Effect |
|---|---|
Justify::Start | Pack children toward start (default) |
Justify::Center | Center in available space |
Justify::End | Pack toward end |
Justify::SpaceBetween | Even space between children (none at edges) |
Justify::SpaceAround | Even space around each child |
Justify::SpaceEvenly | Equal space between and around children |
BorderStyle
| Value | Appearance |
|---|---|
BorderStyle::Plain | ─ │ ┌ ┐ └ ┘ |
BorderStyle::Rounded | ─ │ ╭ ╮ ╰ ╯ |
BorderStyle::Double | ═ ║ ╔ ╗ ╚ ╝ |
BorderStyle::Thick | ━ ┃ ┏ ┓ ┗ ┛ |
BorderStyle::LightDoubleDashed | Dashed light border |
BorderStyle::HeavyDoubleDashed | Dashed heavy border |
BorderStyle::LightTripleDashed | Triple-dashed light |
BorderStyle::HeavyTripleDashed | Triple-dashed heavy |
BorderStyle::LightQuadrupleDashed | Quadruple-dashed light |
BorderStyle::HeavyQuadrupleDashed | Quadruple-dashed heavy |
Theme System
App-Wide Theme
App::new()
.theme(Theme::one_dark())
.mount(Root)
.run()If omitted, Theme::default() applies automatically.
ThemeProvider Widget
Applies theme defaults to a subtree without affecting unset widget colors:
ThemeProvider::new(Theme::dracula())
.child(my_sidebar_element)Style precedence: explicit widget style > ThemeProvider theme > widget defaults.
Note: base-style background is not auto-injected into every descendant. Only widgets that read theme styles apply them.
Named Presets
Theme::default()
Theme::one_dark()
Theme::dracula()
Theme::nord()
Theme::gruvbox()
Theme::catppuccin()
Theme::tokyo_night()
Theme::solarized_dark()
Theme::monokai()Builder API for Custom Themes
// Fast path: define a theme from foreground, background, and accent
let my_theme = Theme::custom(
Color::rgb(0xE0, 0xE0, 0xE0),
Color::rgb(0x10, 0x10, 0x15),
Color::rgb(0xFF, 0x80, 0x00),
);
// Start from a preset, override only what you need
let my_theme = Theme::one_dark()
.primary(Style::new().fg(Color::rgb(0xE0, 0xE0, 0xE0)).bg(Color::rgb(0x10, 0x10, 0x15)))
.accent(Style::new().fg(Color::rgb(0xFF, 0x80, 0x00)))
.selection(Style::new().bg(Color::rgb(0x24, 0x1A, 0x0C)))
.hover(Style::new().bg(Color::rgb(0x18, 0x18, 0x22)));
// Opt in to focused text recoloring on specific text surfaces
let my_theme = Theme::one_dark()
.input(InputPalette {
focus: Style::new().fg(Color::rgb(0xFF, 0xC0, 0x66)).bold(),
})
.text_area(TextAreaPalette {
focus: Style::new().fg(Color::rgb(0xC3, 0xE8, 0x8D)),
})
.document_view(DocumentViewPalette {
focus: Style::new().fg(Color::rgb(0x8B, 0xD5, 0xFF)),
});
// Full control over all sub-palettes
let full_custom = Theme::default()
.primary(Style::new().fg(Color::White).bg(Color::Black))
.accent(Style::new().fg(Color::Cyan))
.selection(Style::new().fg(Color::Black).bg(Color::Cyan))
.hover(Style::new().bg(Color::indexed(236)))
.scrollbar(ScrollbarPalette {
track: None,
thumb: Color::DarkGray,
thumb_focus: Some(Color::White),
})
.splitter(SplitterPalette { hover: Color::Blue, active: Color::Cyan })
.file_icons(FileIconPalette { /* ... */ })
.git_status(GitStatusPalette { /* ... */ });Theme Hot Reload (feature: theme-reload)
Enable the feature and run the example:
cargo run --example theme_hot_reload --features theme-reloadExample TOML theme file with extends plus style/color overrides:
extends = "one_dark"
[primary]
fg = "#E0E0E0"
bg = "#101015"
[accent]
fg = "#FF8000"Watcher wiring in the example is intentionally simple:
ThemeWatchermonitors the theme file for on-disk updates.load_theme_from_tomlrebuilds aThemefrom the current TOML file.- A periodic app message drives polling; when a change is detected, the app reloads and applies the new theme.
Note: watcher path matching includes a filename fallback for editor save-via-rename flows. If multiple watchers target sibling files with the same basename, events may cross-trigger.
Limitation: Theme::extensions (typed extension data from with_extension) is not TOML-reloadable and remains programmatic.
Typed Theme Extensions
When your app has semantic theme tokens that do not fit the framework's core palettes, store them inside Theme rather than a parallel global cache.
use tui_lipan::prelude::*;
#[derive(Clone, Debug, PartialEq)]
struct BrandTheme {
shell_badge: Style,
}
let theme = Theme::one_dark().with_extension(BrandTheme {
shell_badge: Style::new().fg(Color::rgb(0x7D, 0xCF, 0xFF)),
});Read them from components with ctx.theme_extension::<T>():
let brand = ctx.theme_extension::<BrandTheme>().expect("brand theme installed");
Text::new("shell").style(brand.shell_badge)This keeps app-specific tokens inside the same ThemeProvider tree as the framework palettes, so theme switching and invalidation remain centralized.
Theme Fields
| Field | Type | Purpose |
|---|---|---|
primary | Style | Base text and background |
accent | Style | Interactive emphasis for hover/cursors/controls |
selection | Style | Selected/current state |
focus | Style | Focused widget chrome and focus affordances |
hover | Style | Optional row/surface hover state |
border | Style | Frame and divider color |
muted | Style | Placeholders, disabled text, indicators |
diff | DiffPalette | DiffView line/word/marker/separator/patch-header styles |
document | DocumentPalette | DocumentView/markdown heading/link/code/table styles |
syntax | SyntaxPalette | Theme-aware syntect token recoloring |
input | InputPalette | Explicit focused-content styling for Input and input-backed composites |
text_area | TextAreaPalette | Explicit focused-content styling for TextArea |
document_view | DocumentViewPalette | Explicit focused-content styling for DocumentView |
hex_area | HexAreaPalette | Explicit focused-content/cursor styling for HexArea |
terminal | TerminalPalette | Explicit focused-content styling for Terminal |
scrollbar | ScrollbarPalette | Scrollbar track/thumb colors |
splitter | SplitterPalette | Splitter handle colors |
file_icons | FileIconPalette | File icon colors |
git_status | GitStatusPalette | Git status badge colors |
Notes:
Theme::custom(fg, bg, accent)derivesaccent,selection,focus,border,muted,diff,document,syntax,scrollbar, andsplitterdefaults from those three colors.- Generic
hoveris disabled by default. Opt in withTheme::hover(...)when you want row/surface hover feedback. - Set
Theme::focus(Style::default())when you want the theme itself to stay visually quiet on focus while still allowing widgets to opt into explicit.focus_style(...)overrides. - Buttons and other control-emphasis states use
accent, notselection, so selection styling stays independent from interactive styling. - Text-oriented widgets keep their normal text color on focus by default. Theme
focusapplies to focus chrome (borders, focus affordances), whileinput.focus,text_area.focus,document_view.focus,hex_area.focus, andterminal.focusopt into focused content styling. - Widget APIs follow the same split: use
.focus_style(...)for focus chrome and.focus_content_style(...)when you want focused text/content to change. DiffViewnow usestheme.diffby default unless you explicitly overridediff_style(...).DocumentView::markdown()now usestheme.documentby default unless you explicitly override formatter/document styles.SyntectStrategynow accepts a theme-nativesyntaxpalette as a hybrid recoloring layer on top of the selected syntect theme.SyntaxPaletteincludes separateconstant,builtin, andparameterstyles so syntect can distinguish booleans/null-like values, stdlib names, and function parameters from numbers or regular identifiers.
Color Contrast
Available in tui_lipan::utils::color_contrast:
use tui_lipan::utils::color_contrast;
// Pick a readable foreground for a given background.
// Tries: preferred → lightness-adjusted preferred → black or white.
let fg = color_contrast::readable_text_color(preferred_fg, bg);
// Simply pick black or white (Material Design / Apple HIG approach)
let fg = color_contrast::black_or_white(bg);
// Adjust a color's lightness to meet a contrast target (preserves hue)
let fg = color_contrast::adjust_for_contrast(fg, bg, 4.5);
// WCAG 2.1 metrics
let ratio = color_contrast::contrast_ratio(fg, bg);
let lum = color_contrast::relative_luminance(color);
// Color transforms (general-purpose, not used in readability logic)
let comp = color_contrast::complementary_color(color);
let inv = color_contrast::inverse_color(color);App-level contrast policy:
App::new()
.contrast_policy(ContrastPolicy::Wcag) // default: WCAG 2.1 auto-adjust
// or:
.contrast_policy(ContrastPolicy::BlackOrWhite) // keep readable fg, else snap to black/white
// or:
.contrast_policy(ContrastPolicy::Apca) // APCA perceptual contrast
// or:
.contrast_policy(ContrastPolicy::Off) // preserve explicit colors exactlyPer-widget override via .contrast_policy(...) on: Button, Checkbox, Input, TextArea, List, Table, Tabs, DraggableTabBar, ProgressBar.
You can also force contrast on a specific style after patching/theme resolution:
let label_style = Style::new()
.transform_fg(ColorTransform::Dim(0.35))
.contrast_policy(ContrastPolicy::BlackOrWhite);Color Gradients
use tui_lipan::prelude::*; // re-exports ColorGradient, GradientDirection, GradientRange
let gradient = ColorGradient::new(vec![
(0.0, Color::rgb(0, 128, 255)),
(0.5, Color::rgb(128, 0, 255)),
(1.0, Color::rgb(255, 0, 128)),
]);
// Use in Sparkline, ProgressBar, Table heatmaps, etc.
ProgressBar::new(0.7).filled_gradient(gradient)