Theming

Use OKLCH semantic tokens, roles, surfaces, fields, and layout values across your application.

Rust
1use herogpui::theme::{ThemeProvider, ActiveTheme};

Every color in HeroGPUI is a semantic token resolved from the active Theme. Base values are OKLCH, and derived values mix them in Oklab with fixed weights, so overriding one token moves everything derived from it.

The ActiveTheme trait reaches the tokens from any GPUI context — &App, &mut App, Context<T> (they all deref): cx.colors() for the palette, cx.role(Color::Accent) for a role, cx.layout() for the layout tokens.

Rust
1fn my_view(cx: &App) -> impl IntoElement {
2    let primary = cx.role(Color::Accent);
3    div().bg(primary.color).text_color(primary.foreground)
4}

Base tokens

Nine base tokens feed the rest of the palette. The system uses semantic tokens instead of numbered scales.

Base tokens
TokenRustLight / darkValue & notes
--backgroundcolors.backgroundoklch(0.9702 0 0)the page
--foregroundcolors.foregroundoklch(0.2103 0.0059 285.89)body text
--mutedcolors.mutedoklch(0.5517 0.0138 285.94)de-emphasised body text and icons
--scrollbarcolors.scrollbarforeground at 15% alphathe thumb
--bordercolors.borderoklch(0.9 0.004 286.32)one step darker than --separator
--separatorcolors.separatoroklch(0.92 0.004 286.32)rules between rows
--focuscolors.focussame as --accentthe focus ring
--linkcolors.linksame as --foregroundlink text
--backdropcolors.backdropblack at 50% alphathe scrim behind modals and drawers

Containers

Layered surfaces, one step at a time away from the page: surface for components that sit inline, overlay for the ones that float.

Container tokens
TokenRustLight / darkValue & notes
--surfacecolors.surface.background / .foregroundwhitenon-floating components: cards, accordions, disclosure groups
--surface-secondarycolors.surface_secondaryoklch(0.9524 0.0013 286.37)first step away from the page
--surface-tertiarycolors.surface_tertiaryoklch(0.9373 0.0013 286.37)second step away from the page
--overlaycolors.overlay.background / .foregroundwhitefloating components: tooltips, popovers, modals, menus — --overlay *is* --surface in dark mode; the shadow separates them
--segmentcolors.segment.background / .foregroundwhitethe selected segment of a segmented control (tabs, toggle groups)

Four more are derived on ThemeColors with the stylesheet's weights:

  • background_secondary() color-mix(in oklab, var(--background) 96%, var(--foreground) 4%)
  • background_tertiary()… 92% / 8%
  • background_inverse()var(--foreground)
  • separator_secondary() / separator_tertiary() var(--surface) mixed 85/15, then 81/19, toward var(--surface-foreground)

Roles

Five semantic roles cover neutral, accent and status colors: default, accent, success, warning and danger. Each RoleColor carries a base value and readable foreground; its other shades derive from the stylesheet's color-mix weights.

Semantic roles
RoleRustLight / darkLight valueDark value
--defaultcolors.defaultoklch(0.94 0.001 286.375)oklch(0.274 0.006 286.033)
--accentcolors.accentoklch(0.6204 0.195 253.83)oklch(0.6204 0.195 253.83)
--successcolors.successoklch(0.7329 0.1935 150.81)oklch(0.7329 0.1935 150.81)
--warningcolors.warningoklch(0.7819 0.1585 72.33)oklch(0.8203 0.1388 76.34)
--dangercolors.dangeroklch(0.6532 0.2328 25.74)oklch(0.594 0.1967 24.63)

These are the values and methods a RoleColor derives:

Rust
1let accent = cx.role(Color::Accent);
2
3accent.color             // --accent
4accent.foreground        // --accent-foreground
5accent.hover()           // color-mix(in oklab, var(--accent) 90%, var(--accent-foreground) 10%)
6accent.soft()            // color-mix(in oklab, var(--accent) 15%, transparent)
7accent.soft_hover()      // color-mix(in oklab, var(--accent) 20%, transparent)
8accent.soft_foreground(cx.colors().foreground)
9                         // color-mix(in oklab, var(--accent) 70%, var(--foreground) 30%)

The token helpers are just these fields and methods, so any view paints with the same vocabulary the components use:

Rust
1div().bg(cx.colors().surface.background)
2div().bg(cx.colors().accent.soft())
3div().text_color(cx.colors().muted)

Fields

Form-field tokens are kept separate from buttons so inputs can be styled independently. field.hover() mixes the background 90/2 toward its foreground (the weights are the stylesheet's, normalised by CSS), and field.focus() is var(--field-background) — the ring and border do the pointing.

Field tokens
TokenRustLight / darkValue & notes
--field-backgroundfield.backgroundwhitein dark mode this is the surface colour, not --default, which is two steps lighter
--field-foregroundfield.foregroundoklch(0.2103 0.0059 285.89)typed text
--field-placeholderfield.placeholderthe muted tokenplaceholder text
--field-borderfield.bordertransparent--field-border-width is 0 by default; the token exists for a caller who gives their fields a border

Layout tokens

Layout uses one --radius base with calculated steps and component-semantic shadows. Read these values through cx.layout().

Layout tokens
TokenRustValue & notes
--spacinglayout.spacing0.25remthe spacing unit
--radiuslayout.radius0.5rem (8px)the base every other radius is calculated from
--radius-xs … --radius-4xllayout.radius_xs() … radius_4xl()0.25× … 4× --radiusthe calculated steps
capped(r)layout.capped(r)min(32px, r)caps rounded-* and rounded-full with min() so an oversized --radius cannot distort a component
--field-radiuslayout.field_radiuscalc(var(--radius) * 1.5) = 12pxevery form field
--border-widthlayout.border_width1pxthe weight a separator draws; there is no per-instance override
--field-border-widthlayout.field_border_width0pxfields separate with their background, not a border
--disabled-opacitylayout.disabled_opacity0.5
--ring-offset-widthlayout.ring_offset_width2px
--surface-shadowlayout.surface_shadowcards, accordions and other inline containersdark mode drops all three shadows
--overlay-shadowlayout.overlay_shadowtooltips, popovers, modals and menusdark mode keeps only a 1px inset highlight, reproduced as a hairline border
--field-shadowlayout.field_shadowinputs and other form controls
--skeleton-animationlayout.skeleton_animationshimmer (default)SkeletonAnimation::{Shimmer, Pulse, None}
--tooltip-delaylayout.tooltip_delay_ms1500msTooltip reads it as its default delay
--tooltip-close-delaylayout.tooltip_close_delay_ms500ms