Skip to content

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.

rust
Style::new()
    .fg(Color::Blue)
    .bg(Color::indexed(235))
    .bold()
    .italic()
    .underline()
    .dim()
    .reverse()
MethodEffect
.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:

rust
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: Style has .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.

rust
// ✅ 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: fg does NOT work this way. Each widget must set its own fg if 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:

rust
// ❌ 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

PrioritySource
HighestExplicit widget style (set directly on the widget)
ThemeProvider (applied to the subtree)
Parent container bg (painted in the terminal buffer)
LowestApp-level theme default

Colors

rust
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 / parent

Color::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 B50B950 (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

rust
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:

MethodEffect
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
rust
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::Opacity blends the foreground toward the resolved cell background. It has no effect when the cell background is Color::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 to App::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.

rust
EffectScope::new()
    .effect(VisualEffect::PaletteQuantize {
        palette: EffectPalette::Gameboy,
    })
    .effect(VisualEffect::Scanlines {
        strength: 0.18,
        spacing: 2,
    })
    .child(content)

Common variants:

TypePurpose
VisualEffect::ColorTransformApply relative color transforms (Dim, Lighten, Opacity, Tint) to fg/bg of each cell. Constructors: dim, lighten, tint, transform_fg, transform_bg
VisualEffect::ContrastPolicyApply ContrastPolicy to ensure text legibility
VisualEffect::MonochromeDesaturation / grayscale conversion
VisualEffect::PaletteQuantizeReduce colors to a preset or custom palette
VisualEffect::ScanlinesStatic row-based dimming mask
VisualEffect::RainbowWaveAnimated color cycling by position and frame phase, blended back into the subtree
VisualEffect::GradientSine-eased mirrored ColorGradient wash sampled in scope-local coordinates; optional animation via speed / frame phase
VisualEffect::RetroCrtRetro preset built from palette, scanline, and flicker primitives
VisualEffect::ClippedBounds and/or CellMask to restrict another effect - see widgets/effects.md

Supporting enums:

EnumVariants
EffectAxisHorizontal, Vertical, Diagonal
EffectPaletteCga, Gameboy, Amber, Green, Custom(Vec<Color>)
RetroPresetAmber, 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, so hover_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 toward color. At alpha = 1.0, both channels become color; use hover_effect(VisualEffect::transform_fg(ColorTransform::Tint(color, 1.0))) when you only want to recolor text.

Layout Primitives

Length

ValueMeaning
Length::AutoSize 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

rust
// 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:

ValueEffect
Align::StartTop/left (default)
Align::CenterCentered
Align::EndBottom/right
Align::StretchFill available space

Justify

Main-axis alignment for stacks:

ValueEffect
Justify::StartPack children toward start (default)
Justify::CenterCenter in available space
Justify::EndPack toward end
Justify::SpaceBetweenEven space between children (none at edges)
Justify::SpaceAroundEven space around each child
Justify::SpaceEvenlyEqual space between and around children

BorderStyle

ValueAppearance
BorderStyle::Plain─ │ ┌ ┐ └ ┘
BorderStyle::Rounded─ │ ╭ ╮ ╰ ╯
BorderStyle::Double═ ║ ╔ ╗ ╚ ╝
BorderStyle::Thick━ ┃ ┏ ┓ ┗ ┛
BorderStyle::LightDoubleDashedDashed light border
BorderStyle::HeavyDoubleDashedDashed heavy border
BorderStyle::LightTripleDashedTriple-dashed light
BorderStyle::HeavyTripleDashedTriple-dashed heavy
BorderStyle::LightQuadrupleDashedQuadruple-dashed light
BorderStyle::HeavyQuadrupleDashedQuadruple-dashed heavy

Theme System

App-Wide Theme

rust
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:

rust
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

rust
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

rust
// 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:

bash
cargo run --example theme_hot_reload --features theme-reload

Example TOML theme file with extends plus style/color overrides:

toml
extends = "one_dark"

[primary]
fg = "#E0E0E0"
bg = "#101015"

[accent]
fg = "#FF8000"

Watcher wiring in the example is intentionally simple:

  • ThemeWatcher monitors the theme file for on-disk updates.
  • load_theme_from_toml rebuilds a Theme from 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.

rust
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>():

rust
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

FieldTypePurpose
primaryStyleBase text and background
accentStyleInteractive emphasis for hover/cursors/controls
selectionStyleSelected/current state
focusStyleFocused widget chrome and focus affordances
hoverStyleOptional row/surface hover state
borderStyleFrame and divider color
mutedStylePlaceholders, disabled text, indicators
diffDiffPaletteDiffView line/word/marker/separator/patch-header styles
documentDocumentPaletteDocumentView/markdown heading/link/code/table styles
syntaxSyntaxPaletteTheme-aware syntect token recoloring
inputInputPaletteExplicit focused-content styling for Input and input-backed composites
text_areaTextAreaPaletteExplicit focused-content styling for TextArea
document_viewDocumentViewPaletteExplicit focused-content styling for DocumentView
hex_areaHexAreaPaletteExplicit focused-content/cursor styling for HexArea
terminalTerminalPaletteExplicit focused-content styling for Terminal
scrollbarScrollbarPaletteScrollbar track/thumb colors
splitterSplitterPaletteSplitter handle colors
file_iconsFileIconPaletteFile icon colors
git_statusGitStatusPaletteGit status badge colors

Notes:

  • Theme::custom(fg, bg, accent) derives accent, selection, focus, border, muted, diff, document, syntax, scrollbar, and splitter defaults from those three colors.
  • Generic hover is disabled by default. Opt in with Theme::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, not selection, so selection styling stays independent from interactive styling.
  • Text-oriented widgets keep their normal text color on focus by default. Theme focus applies to focus chrome (borders, focus affordances), while input.focus, text_area.focus, document_view.focus, hex_area.focus, and terminal.focus opt 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.
  • DiffView now uses theme.diff by default unless you explicitly override diff_style(...).
  • DocumentView::markdown() now uses theme.document by default unless you explicitly override formatter/document styles.
  • SyntectStrategy now accepts a theme-native syntax palette as a hybrid recoloring layer on top of the selected syntect theme.
  • SyntaxPalette includes separate constant, builtin, and parameter styles 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:

rust
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:

rust
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 exactly

Per-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:

rust
let label_style = Style::new()
    .transform_fg(ColorTransform::Dim(0.35))
    .contrast_policy(ContrastPolicy::BlackOrWhite);

Color Gradients

rust
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)

MIT OR Apache-2.0