Customization
Create a named HeroGPUI theme by overriding semantic colors, layout tokens, and component recipes.
Start from a light or dark Theme and override the semantic colors, layout values, or ThemeBuilder::components recipes your application needs. Theme::builder(id, base) names the result, and derived color values follow the base token they came from. Empty component styles keep stock HeroUI metrics.
The builder
use gpui::px;
use herogpui::core::{oklch, Color, FieldVariant};
use herogpui::theme::{
snow, ButtonStyle, ComponentTheme, ComponentThemes, SelectStyle, SliderStyle, Theme,
};
let violet = Theme::builder("violet", Theme::light())
.accent(oklch(0.55, 0.23, 295.0)) // hover / soft / focus all derive
.role(Color::Success, oklch(0.73, 0.19, 150.0), snow())
.radius(px(6.)) // field_radius follows at 1.5x
.components(
ComponentThemes::default()
.slider(ComponentTheme::new(SliderStyle::default().radius(px(9999.))))
.select(
ComponentTheme::new(SelectStyle::default().variant(FieldVariant::Secondary))
.recipe("compact", SelectStyle::default().height(px(28.))),
)
.button(ComponentTheme::new(ButtonStyle::default()).recipe(
"compact",
ButtonStyle::default().style(|el| el.h(px(28.)).px(px(10.))),
)),
)
.build();
herogpui::theme::set_theme(violet, cx);
Button::new("ok").label("OK").recipe("compact");Every method the builder exposes:
| Method | What it sets |
|---|---|
id(id) | Names the theme — the id `use_theme(id, cx)` activates it by. |
appearance(appearance) | Light or dark — decides which shadow set and hairline the layout tokens use. |
radius(px) | `--radius`. `--field-radius` follows at 1.5× unless overridden afterwards. |
field_radius(px) | `--field-radius` on its own. |
border_width(px) | `--border-width`, the weight a separator draws. |
disabled_opacity(f32) | `--disabled-opacity`. |
background / foreground / muted | The base page tokens. |
border / separator / focus / link / backdrop | The remaining base tokens, each set on its own. |
surface(bg, fg) | `--surface` / `--surface-foreground` — cards, accordions, disclosure groups. |
surface_levels(secondary, tertiary) | `--surface-secondary` and `--surface-tertiary` together. |
overlay(bg, fg) | `--overlay` — tooltips, popovers, modals, menus. |
segment(bg, fg) | `--segment` — the selected segment of a segmented control. |
role(Color, color, foreground) | Any role's base value and foreground, by the typed `Color` role. `Color::Default` also re-seeds `field.background`. |
accent(color) | `--accent` and a foreground derived for readability when you do not supply one. `--focus` tracks it. |
field(bg, fg) / field_placeholder / field_border | The field tokens individually. |
components(ComponentThemes) | Typed component defaults and named recipes. Resolved live from ThemeProvider each frame. Not a ThemeDocument JSON key. |
build() | Returns the `Theme`. |
Overriding a base token
Override a base token and its derived values follow. The override replaces the input while preserving the mix weights: a role's hover and soft ratios carry over, --focus keeps tracking --accent, and a foreground override flows into soft_foreground at render time without rebuilding the theme.
// Override one base token; every derived value follows.
let violet = Theme::builder("violet", Theme::light())
.accent(oklch(0.55, 0.23, 295.0))
.build();
// `accent.hover()` and `accent.soft()` are the same color-mix
// expressions, so they move with the base color.Registering the result
set_theme(theme, cx) registers the theme under its id, activates it, and schedules every open window to repaint. The built-ins stay registered, so use_theme("light", cx) switches back — and a theme registered with set_theme is thereafter switchable to by id like any other. See Dark Mode for the switching rules. Renderers resolve Theme.components from the active provider each frame, so a later set_theme updates every .recipe("name") call site without rebuilding those builders.
Colour maths
The color functions used by the theme crate are public in herogpui-core. Use them to create OKLCH values, mix colors in Oklab and choose readable foregrounds:
use herogpui::core::{
oklch, oklcha, mix_oklab, soft_mix, with_alpha, readable_color,
};
let brand = oklch(0.55, 0.23, 295.0); // lightness, chroma, hue
let translucent = oklcha(0.55, 0.23, 295.0, 0.6);
let hover = mix_oklab(brand, foreground, 0.10); // 10% toward the foreground
let soft = soft_mix(brand, 0.15); // 15% over transparent
let faint = with_alpha(brand, 0.4);
let label = readable_color(brand); // a readable foreground for it