Guide
Theming
Every colour, radius, and type style comes from the active theme, including yours.
There is no styling layer
Every colour, radius, spacing value, shadow, and type style resolves through cx.theme(). gpui-ai adds no styling layer of its own and contains no hardcoded colours, so light and dark, the 55 bundled themes, a theme of your own, and live token editing all work without a single per-component override.
use gpui::prelude::*;
use gpui_component::ActiveTheme as _;
fn panel(cx: &App) -> impl IntoElement {
div()
// Every colour, radius and type style comes from the active theme.
// There is no gpui-ai styling layer to override and no colour of its
// own to disagree with yours.
.bg(cx.theme().secondary)
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.text_color(cx.theme().muted_foreground)
.child("Ran three tools")
}
Layout resolves through semantic spacing tokens and the rem scale rather than raw pixels, which is what makes window zoom work. A test in this repository enforces it, so a stray px(12.) fails the build rather than reaching a release.
The bundled themes are files
The 55 themes on the Themes page are JSON files in themes/, embedded at build time and registered at startup. Adding a file adds a theme; there is no table of presets to update alongside it.
Each file declares a name, a mode, and its colours, and may declare a corner radius, a shadow flag, and a base font size.
Anything a theme leaves out is worth knowing about. Applying a theme writes only the metrics that theme names, and what it leaves behind is the previous theme’s value rather than the default. An application that installs one theme and keeps it never sees this. One that lets a person switch does: a theme asking for 14 px type leaves every theme chosen after it at 14 px until the process restarts. Three of the 55 bundled themes declare metrics and the rest do not, so the sample below puts back what the incoming theme does not mention.
Using your own
Load a theme pack into the registry and switch to one of the themes in it. The format is the one the bundled packs use, so a theme downloaded from the Themes page is a working starting point.
use gpui::App;
use gpui_component::theme::{Theme, ThemeConfig, ThemeMode, ThemeRegistry};
use std::rc::Rc;
/// Registers a theme pack and switches to one of the themes in it.
///
/// A pack is the same JSON the bundled themes use: a name and a `themes`
/// array, each entry declaring a mode, its colours, and optionally a radius,
/// a shadow flag, and a base font size.
fn install(pack: &str, name: &str, cx: &mut App) -> anyhow::Result<()> {
ThemeRegistry::global_mut(cx).load_themes_from_str(pack)?;
let config = ThemeRegistry::global(cx)
.themes()
.get(name)
.cloned()
.ok_or_else(|| anyhow::anyhow!("the pack declares no theme called {name}"))?;
let mode = config.mode;
let theme = Theme::global_mut(cx);
match mode {
ThemeMode::Dark => theme.dark_theme = config.clone(),
_ => theme.light_theme = config.clone(),
}
Theme::change(mode, None, cx);
restore_unset_metrics(&config, cx);
Ok(())
}
/// Puts back the metrics the incoming theme does not mention.
///
/// Applying a theme writes only the metrics that theme names, and what it
/// leaves behind is the *previous* theme's value rather than the default. In
/// an application that installs one theme and keeps it that never shows. In
/// one that lets a person switch, a theme asking for 14px type leaves every
/// theme chosen after it at 14px until the process restarts.
fn restore_unset_metrics(config: &ThemeConfig, cx: &mut App) {
let defaults = Theme::default();
let theme = Theme::global_mut(cx);
if config.font_size.is_none() {
theme.font_size = defaults.font_size;
}
if config.mono_font_size.is_none() {
theme.mono_font_size = defaults.mono_font_size;
}
if config.radius.is_none() {
theme.radius = defaults.radius;
}
if config.radius_lg.is_none() {
theme.radius_lg = defaults.radius_lg;
}
if config.shadow.is_none() {
theme.shadow = defaults.shadow;
}
// The Base layer keeps its own copy of the radius, so a scrollbar would go
// on painting with the corners the last theme gave it.
Theme::sync_base(cx);
}
The website is painted from the same numbers as the demos inside it. That is not a coincidence to admire: it is the check. If a theme reads badly on this site, it reads badly in an application.