ratatui-themekit 0.6.1

Semantic theme system for ratatui — 11 themes, widget builders, full-screen canvas, zebra rows, state-aware styles, NO_COLOR
Documentation

ratatui-themekit

Crates.io docs.rs CI License: MIT

Semantic theme system for ratatui.

Showcase

Stop hardcoding Color::Rgb(...) everywhere. Define what colors mean and let themes provide the actual values.

The Problem

// Without themekit — colors scattered everywhere, no consistency
let style = Style::default().fg(Color::Rgb(166, 227, 161)); // what is this?
let err = Style::default().fg(Color::Rgb(243, 139, 168));   // hope I remember

The Solution

use ratatui_themekit::{Theme, ThemeExt, CatppuccinMocha};

let t = CatppuccinMocha;

// Span builders
let title = t.fg_accent("Dashboard").bold().build();

// Line compositor — no more vec![] + .build() on every span
let line = t.line().accent_bold("App").dim(" | ").success("Ready").build();

// Block builder — themed borders, title, focus state
let panel = t.block(" Status ").focused(true).build();

// Status bar — key-value pairs
let status = t.status_line().kv("Mode", "Normal").kv("Ln", "42").build();

// Widget style bundles — Table, List, Tabs, Gauge, State
let ts = t.table_styles(); // header, row, highlight, stripe
let ls = t.list_styles();  // base, highlight, symbol

Quick Start

cargo add ratatui-themekit
use ratatui::text::Line;
use ratatui_themekit::{Theme, ThemeExt, resolve_theme};

let theme = resolve_theme("catppuccin");
let t = theme.as_ref();

// Build themed spans — never touch Style::default() again
let header = Line::from(vec![
    t.fg_accent("App v1.0").bold().build(),
    t.fg_border(" | ").build(),
    t.fg_bright("Ready").build(),
]);

// Widget styles (border_style, title_style)
let border = t.style_border();
let title = t.style_accent();

Builders

Import ThemeExt to get all builders on any Theme:

Spans

t.fg_accent("title").bold().build()     // semantic color + modifiers
t.fg_success("ok").italic().build()     // success, error, warning, info, dim, bright
t.fg_added("+line").build()             // diff colors
t.badge(" RUN ", Color::Green).build()  // text on colored background

Line Compositor

// Compose multi-span lines without vec![] boilerplate
let line = t.line()
    .accent_bold("Dashboard")
    .dim(" | ")
    .success_bold("3 passed")
    .dim(", ")
    .error_bold("1 failed")
    .build();

Block Builder

let panel = t.block(" Dashboard ").build();            // themed borders + title
let active = t.block(" Active ").focused(true).build(); // accent-colored borders
let bare = t.block_plain().build();                     // no title

Status Line

let status = t.status_line()
    .kv("Mode", "Normal")
    .kv("File", "main.rs")
    .kv_colored("Build", "passing", t.success())
    .separator(" | ")
    .build();

Widget Style Bundles

// Table — header, row, highlight, zebra striping
let ts = t.table_styles();
let striped = zebra_rows(rows, ts.stripe);

// List — base, highlight, symbol
let ls = t.list_styles();

// Tabs — active, inactive
let tabs = t.tab_styles();

// Gauge — filled, base
let gs = t.gauge_styles();

// Input — text, placeholder, cursor, prompt, border, border_focused
let is = t.input_styles();

// Scrollbar — track, thumb
let sb = t.scrollbar_styles();

// Notifications — info, success, warning, error, body, background
let ns = t.notification_styles();

State-Aware Styles

// Resolve style based on widget state (focused, selected, disabled)
let ss = t.state_styles();
let style = ss.resolve(is_focused, is_selected, is_disabled);

Progress Bar & Separator

t.bar(75).width(20).build()   // ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱ 75%
t.separator_line(40)           // · · · · · · · · · · · ·

Full-Screen Canvas

// Paint themed background on the entire terminal — one line, all widgets inherit it.
// Plugins/widgets can still override bg on their own elements.
frame.render_widget(
    Block::default().style(t.style_base()),  // bg + fg from theme
    frame.area(),
);

Style Helpers

// For widget APIs that take Style (border_style, title_style)
t.style_accent()   t.style_border()   t.style_error()
t.style_success()  t.style_warning()  t.style_info()
t.style_bright()   t.style_dim()      t.style_surface()
t.style_base()     // bg(background) + fg(text) — full-screen canvas

20 Semantic Color Slots

Category Slots Purpose
Brand accent, accent_dim Primary UI color, subtle highlights
Text text, text_dim, text_bright Default, muted, emphasized text
Status success, error, warning, info Semantic status indicators
Diff diff_added, diff_removed, diff_context Code diff rendering
Structure border, surface, background Borders, focus highlight, app background
Derived block_*, indicator_* Auto-derived from core slots

11 Built-in Themes

Theme ID Style
Catppuccin Mocha catppuccin Warm dark, pastel accents (default)
Dracula dracula Dark, vivid purples and greens
Nord nord Arctic blue-gray, calm
Gruvbox Dark gruvbox Retro warm, earthy tones
One Dark one-dark Atom's classic blue
Solarized Dark solarized Precision-engineered
Tailwind Dark tailwind Tailwind CSS palette
Tokyo Night tokyo-night Vivid blue accents
Rosé Pine rose-pine Muted, elegant rose tones
Terminal Native terminal Named ANSI colors only
No Color no-color All Color::Reset for NO_COLOR

Themes are pure data (ThemeData constants) — zero boilerplate, zero code duplication.

See all themes rendered: Theme Gallery

Custom Themes

Implement the Theme trait — 15 required methods, 12+ derived automatically. Override background() to set a custom app background (defaults to Color::Reset):

use ratatui::style::Color;
use ratatui_themekit::Theme;

struct TokyoNight;

impl Theme for TokyoNight {
    fn name(&self) -> &str { "Tokyo Night" }
    fn id(&self) -> &str { "tokyo-night" }
    fn accent(&self) -> Color { Color::Rgb(122, 162, 247) }
    fn accent_dim(&self) -> Color { Color::Rgb(61, 89, 161) }
    fn text(&self) -> Color { Color::Rgb(169, 177, 214) }
    fn text_dim(&self) -> Color { Color::Rgb(86, 95, 137) }
    fn text_bright(&self) -> Color { Color::Rgb(195, 202, 235) }
    fn success(&self) -> Color { Color::Rgb(158, 206, 106) }
    fn error(&self) -> Color { Color::Rgb(247, 118, 142) }
    fn warning(&self) -> Color { Color::Rgb(224, 175, 104) }
    fn info(&self) -> Color { Color::Rgb(125, 207, 255) }
    fn diff_added(&self) -> Color { Color::Rgb(158, 206, 106) }
    fn diff_removed(&self) -> Color { Color::Rgb(247, 118, 142) }
    fn diff_context(&self) -> Color { Color::Rgb(86, 95, 137) }
    fn border(&self) -> Color { Color::Rgb(41, 46, 66) }
    fn surface(&self) -> Color { Color::Rgb(30, 32, 48) }
}
// All ThemeExt builders work automatically!

Serde Custom Themes

Load themes from config files with the serde feature:

cargo add ratatui-themekit --features serde
use ratatui_themekit::CustomTheme;

let toml = r#"
name = "My Theme"
id = "my-theme"
accent = { Rgb = [249, 115, 22] }
success = "Green"
error = "Red"
"#;

let theme: CustomTheme = toml::from_str(toml).unwrap();

NO_COLOR Support

Automatically respects the NO_COLOR standard:

use ratatui_themekit::resolve_theme;

// When NO_COLOR is set, resolve_theme returns NoColor automatically
let theme = resolve_theme("catppuccin"); // → NoColor if NO_COLOR is set

Runtime Theme Switching

use ratatui_themekit::{resolve_theme, available_theme_ids};

// List available themes for a settings menu
for id in available_theme_ids() {
    println!("{id}");
}

// Switch at runtime — zero code changes needed
let mut current = resolve_theme("catppuccin");
current = resolve_theme("dracula"); // instant

Design Philosophy

  • Semantic over literal — slots describe meaning, not appearance
  • Builders over manual stylingt.fg_accent("x").bold() not Span::styled("x", Style::default().fg(...))
  • Derived defaults — implement 15 methods, get 27+ color slots
  • Zero opinion on layout — only colors, never widget structure
  • NO_COLOR native — accessibility built in, not bolted on
  • Send + Sync — safe for async TUI architectures

License

MIT