Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Built-in palettes

Twelve palettes ship with the crate. Nine are dark, three are light.

NameModePrimaryNotes
textual-darkdark#0178D4The default. Textual’s own dark palette
textual-lightlight#004578Textual’s light palette
norddark#88C0D0Cool blue-grey, low contrast
gruvboxdark#85A598Warm, muted, high legibility
catppuccin-mochadark#F5C2E7Pastel on a soft dark background
catppuccin-lattelight#8839EFThe light member of the same family
draculadark#BD93F9Purple on near-black
tokyo-nightdark#BB9AF7Deep blue background, violet accents
monokaidark#AE81FFThe classic editor palette
flexokidark#205EA6Paper-inspired, ink-like text
solarized-lightlight#268bd2Solarized’s light half
rose-pinedark#c4a7e7Muted mauve and pine

Using one

extern crate tuile;
use tuile::prelude::*;
fn main() {
theme::set_by_name("nord");                  // returns false for an unknown name
theme::set_by_name("textual-dark");
}

Set it once at startup, before the first frame. Widgets read theme::current() at render time, so changing it mid-run takes effect on the next redraw with no invalidation step.

Getting a palette without installing it globally:

extern crate tuile;
use tuile::prelude::*;
fn main() {
let gruvbox: Theme = theme::builtin("gruvbox").expect("gruvbox ships with the crate");
assert_eq!(gruvbox.name, "gruvbox");
}

theme::builtin(name) resolves the palette for you and returns the finished Theme. The unresolved specs are in theme::BUILTIN, which is what you want when a built-in palette is the starting point for a theme of your own.

Listing them

extern crate tuile;
use tuile::prelude::*;
fn main() {
for name in theme::theme_names() {
    println!("{name}");
}
}

This is what a theme picker in your app should iterate, rather than a hard-coded list that goes stale when the crate adds a palette. The showcase’s Ctrl-T cycle and its Themes page both use it.

Dark and light

spec.dark is a declaration, not a computation. It tells the resolver which direction to derive in: dark themes lift the panel colour slightly toward the contrast colour, light themes do not. The text ramp itself keys off the background’s brightness, so a light palette gets dark text automatically.

If you build a light theme and mark it dark = true, the text will still be readable because the ramp measures the background. The panel and boost surfaces will be subtly wrong.

Overriding one colour

Theme::resolve takes an optional primary override, which re-derives everything that depends on primary while keeping the rest of the palette:

extern crate tuile;
use tuile::prelude::*;
fn main() {
let nord = theme::BUILTIN.iter().find(|s| s.name == "nord").unwrap();
let pink = Theme::resolve(nord, Some(Rgb::hex(0xff79c6)));
assert_ne!(pink.border, theme::builtin("nord").unwrap().border);
}

Borders, cursors, selections and the scrollbar all follow, because each is derived from primary. This is the hue slider on the showcase’s Themes page.

For anything more than the primary, build a spec of your own; the next chapter covers it.

Terminal caveats

Every palette is true colour (24-bit). In a terminal limited to 256 colours, ratatui’s backend maps each colour to the nearest palette entry, which flattens the small lightness steps the design system relies on: a hover row and an unhovered row can land on the same index. The library does not detect this and has no 256-colour palette of its own.

Terminals with a minimum-contrast setting (iTerm2 and some others) adjust foreground colours that sit too close to their background. That is why the library paints filled areas as background colours with space characters rather than as foreground block glyphs; a block glyph gets recoloured by that feature and a painted background does not.