guise
Docs / Foundations
Previewrendered with guise's palette
Blue 6Violet 6Teal 6

Theming

The theme is the single source of truth for color, spacing, radius, and typography. It is installed once as a gpui global and read by every component.

use guise::prelude::*;

Theme::light().init(cx);   // or Theme::dark()

Read it anywhere you have an &App (or a &Context<_>, which derefs to it):

let t = guise::theme::theme(cx);   // &Theme
let accent = t.primary().hsla();

The Theme struct

Field Type Meaning
scheme ColorScheme Light or Dark
palette Palette the 14×10 named-color ramps
primary_color ColorName default fill color (Blue)
primary_shade_light / primary_shade_dark usize filled shade per scheme (6 / 8)
white / black Color absolute white/black
spacing / radius / font_size Scale xs..xl token scales (px)
default_radius Size corner radius when a component doesn't specify one
line_height f32 base line height
font_family SharedString default UI font

Construct with Theme::light() / Theme::dark(), tweak fields, then .init(cx):

let mut theme = Theme::dark();
theme.primary_color = ColorName::Grape;
theme.default_radius = Size::Md;
theme.init(cx);

Colors

Color is a 24-bit RGB triple that converts into gpui's Hsla:

let c = Color::hex("#228be6");     // parse (panics on bad literal — use for constants)
let c = Color::new(34, 139, 230);  // from components
c.hsla();        // opaque gpui::Hsla
c.alpha(0.2);    // gpui::Hsla with alpha
c.contrasting(); // black or white, whichever reads on top of `c`

CSS-style colors

Write colors the way you would in CSS — hex, rgb/rgba, hsl/hsla, or a named color — with the color! macro (compile-time literals) or css(..) (runtime strings). Both produce a gpui Hsla, which carries alpha and drops straight into .bg(..) / .text_color(..) and any guise .color(..).

use guise::prelude::*;

color!(rgb(34, 139, 230))
color!(rgba(34, 139, 230, 0.5))
color!(hsl(210, 80, 52))        // s / l are percentages (no `%` token)
color!(hsla(210, 80, 52, 0.5))
color!(teal)                    // a CSS named color
color!("#228be6")               // any CSS string: hex (#rgb, #rgba, #rrggbb, #rrggbbaa),
color!("hsl(210, 80%, 52%)")    // functional with `%`, or a named color

css("rgba(34,139,230,.5)")?     // runtime parse (config, theme files) -> Result<Hsla>

Bare hex like #228be6 can't be a Rust macro token, so pass hex as a string: color!("#228be6"). Everything else works as bare tokens.

On components. Any colored component's .color(..) accepts a palette ColorName or an explicit CSS color — guise derives the variant shades (filled/light/outline/hover) from a single custom color:

Button::new("go", "Go").color(color!(rgba(112, 72, 232, 1)))
Badge::new("New").variant(Variant::Light).color(color!("#e64980"))

This is the ColorValue type (ColorName and Hsla both Into<ColorValue>). It's wired into Button, Badge, ActionIcon, Alert, and Chip; other components still take a ColorName.

The palette

14 named colors, each a 10-step ramp from lightest (0) to darkest (9) — the Mantine / open-color values.

t.color(ColorName::Teal, 6);    // a specific shade
t.palette.shades(ColorName::Red).get(3);

ColorName variants: Dark, Gray, Red, Pink, Grape, Violet, Indigo, Blue, Cyan, Teal, Green, Lime, Yellow, Orange. ColorName::ALL iterates them; .label() gives the lowercase name.

Re-pin a whole ramp before init with Palette::set_shades — for example, to swap guise's neutral Dark scale for an app's own:

use guise::theme::{Color, Shades};

let mut theme = Theme::dark();
theme.palette.set_shades(
    ColorName::Dark,
    Shades(["#C1C2C5", "#A6A7AB", "#909296", "#5C5F66", "#373A40",
            "#2C2E33", "#25262B", "#1A1B1E", "#141517", "#101113"].map(Color::hex)),
);
theme.init(cx);

Every semantic color (body, surface, text, border, …) then resolves against the replaced ramp.

Semantic colors (scheme-aware)

These resolve differently in light vs. dark mode — use them instead of hard-coding:

Method Use for
t.body() window / page background
t.surface() raised surfaces (Paper, Card, Menu)
t.surface_hover() hover/recessed fill
t.text() primary text
t.dimmed() secondary text
t.border() borders and dividers
t.primary() the primary color at its scheme shade
t.success() positive accents (confirmations, valid states)
t.warning() caution accents
t.danger() errors, destructive actions
t.info() notices and hints
div().bg(t.surface().hsla()).text_color(t.text().hsla())

Override any of these (and the primary accent) app-wide with a CSS color via the with_* builders. Overrides are opaque (alpha is dropped — semantic colors are solid) and apply everywhere the getter is read, so the whole UI restyles:

Theme::dark()
    .with_primary(color!("#7048e8"))          // hex as a string
    .with_body(color!(rgb(11, 11, 15)))
    .with_surface(color!(rgb(20, 20, 28)))
    .with_text(color!("hsl(220, 15%, 92%)"))
    .init(cx);

The setters are named with_* (not body/primary) to avoid clashing with the same-named getters; each accepts anything Into<Hsla> — a color!, css(..), or a palette Color. The full override set: with_primary, with_body, with_surface, with_surface_hover, with_text, with_dimmed, with_border, with_success, with_warning, with_danger, with_info.

Prebuilt themes

Six well-known palettes ship as ready themes — every override slot set:

Theme::catppuccin().init(cx);   // Catppuccin Mocha (dark)
Theme::nord().init(cx);
Theme::tokyonight().init(cx);
Theme::gruvbox().init(cx);
Theme::dracula().init(cx);
Theme::solarized_light().init(cx);

// Or by name (see PRESET_NAMES):
if let Some(theme) = Theme::preset("dracula") { theme.init(cx); }

JSON theme files

Theme::from_json(source) loads a theme from a flat JSON object of string values — deliberately flat so the parser stays dependency-free and files diff cleanly. Colors take any form css() accepts (hex, rgb(), hsl()):

{
  "name": "midnight",
  "scheme": "dark",
  "primary": "#7aa2f7",
  "body": "#1a1b26",
  "surface": "#16161e",
  "surfacehover": "#292e42",
  "text": "#c0caf5",
  "dimmed": "#565f89",
  "border": "#3b4261",
  "success": "rgb(158, 206, 106)",
  "warning": "#e0af68",
  "danger": "#f7768e",
  "info": "#7dcfff",
  "fontfamily": "Inter",
  "radius": "md"
}
let theme = Theme::from_json(&std::fs::read_to_string("theme.json")?)?;
theme.init(cx);

Every key is optional (scheme defaults to dark; unset colors keep the scheme defaults), but unknown keys are rejected with ThemeJsonError::UnknownKey — they're almost always typos. Bad colors and tokens name the offending key in ThemeJsonError::BadValue. name and $schema are accepted and ignored, so files can self-describe.

Sizing tokens

Size (Xs, Sm, Md, Lg, Xl; default Md) indexes three scales:

t.spacing(Size::Md);   // 16.0
t.radius(Size::Sm);    // 4.0
t.font_size(Size::Lg); // 18.0

Defaults (px):

Scale xs sm md lg xl
spacing 10 12 16 20 32
radius 2 4 8 16 32
font_size 12 14 16 18 20

Override a whole scale on the theme before init:

let mut theme = Theme::light();
theme.spacing = Scale::new(8.0, 12.0, 16.0, 24.0, 40.0);
theme.init(cx);

Switching light / dark at runtime

The theme is a mutable global; flip it and request a redraw:

let dark = cx.global::<Theme>().scheme.is_dark();
cx.global_mut::<Theme>().scheme = if dark { ColorScheme::Light } else { ColorScheme::Dark };
cx.refresh_windows();

Because every component reads theme(cx) at render time, the whole UI restyles on the next frame — there is nothing to thread through your views.