-
Notifications
You must be signed in to change notification settings - Fork 9
Guide Theming
The theme is a JSON-backed pub struct Theme(pub serde_json::Value). There is no Rust schema — renderers read paths like action.primary.bg via cx.theme().get_color(...), and your code can do the same.
The two bundled system themes (system-light.json / system-dark.json) ship inside crates/yororen-ui-default-renderer/themes/. The brutalism renderer ships its own themes in crates/yororen-ui-brutalism-renderer/themes/.
- The active palette lives in
cx.global::<GlobalTheme>(), accessed via theActiveThemetrait. -
cx.theme()works on bothAppandContext<'_, T>— any render closure can call it. - The renderer reads the same paths. A missing key returns
None; the renderer falls back to its own default.
yororen_ui::renderer::install(cx, cx.window_appearance());One call: picks system-light.json or system-dark.json by OS appearance, installs the global Theme, and registers the 55 default XxxRenderer impls.
use yororen_ui_default_renderer::{Theme, install_with};
const MY_THEME: &str = include_str!("../themes/my-brand.json");
fn init(cx: &mut gpui::App) {
let theme = Theme::from_json(MY_THEME).expect("valid JSON");
install_with(cx, theme);
}Missing keys fall back to renderer defaults, so you can ship a theme that overrides only the colors that matter to your brand and inherit the rest.
use yororen_ui_default_renderer::{Theme, install_with, system_light, system_dark};
fn init(cx: &mut gpui::App) {
let theme = match cx.window_appearance() {
gpui::WindowAppearance::Dark | gpui::WindowAppearance::VibrantDark => system_dark(),
_ => system_light(),
};
install_with(cx, theme);
}crates/yororen-ui-default-renderer/themes/system-light.json is the canonical starting point:
| Path | What | Used by |
|---|---|---|
surface.base |
default card / input background | Card, TextInput, Panel |
surface.canvas |
window background | root |
surface.raised |
elevated surface | Popover, Modal panel |
surface.popover |
popover panel background | Popover, DropdownMenu |
content.primary |
primary text | Text, Label, Heading |
content.tertiary |
placeholder / hint | TextInput, SearchInput |
content.on_primary |
text on action.primary.bg
|
Primary Button |
border.focus |
focus ring | every focusable primitive |
action.<variant>.bg / .hover_bg / .active_bg / .fg
|
button palette | Button / IconButton / ToggleButton |
status.<kind>.bg / .fg
|
status colors | Badge, Toast, Notification |
tokens.control.<component>.<field> |
per-component geometry | every component |
tokens.motion.duration_fast (150) / .duration_normal (200) |
motion timing | Tooltip, Menu open, transitions |
When you ship a custom theme, the renderers need at least:
surface — base, canvas, raised, sunken, hover, popover
content — primary, secondary, tertiary, disabled, on_primary, on_status
border — default, muted, focus, divider
action.{neutral, primary, danger} — each with bg, hover_bg, active_bg, fg, disabled_bg, disabled_fg
status.{success, warning, danger, error, info, neutral} — each with bg, fg
shadow — elevation_1, elevation_2
tokens.sizes, tokens.radii, tokens.spacing, tokens.typography, tokens.motion, tokens.control.<component> — one sub-object per component. The system-light.json file has every key; ship it as your starting point.
use yororen_ui::theme::ActiveTheme;
let surface = cx.theme().get_color("surface.base").unwrap_or_default();
let pad = cx.theme().get_number("tokens.control.button.horizontal_padding").unwrap_or(16.0);get_color parses hex (#RRGGBB / #RRGGBBAA), {h, s, l, a} objects, and [h, s, l, a] arrays. get_number reads f64 JSON values. Both return Option, so missing keys are safe.
For a "Next theme" toolbar button, call yororen_ui::theme::install(cx, theme) inside the render closure — every frame, or when the user picks a new palette. Idempotent and cheap.
impl Render for ThemeApp {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
yororen_ui::theme::install(cx, self.current_theme());
// … layout using cx.theme() …
}
}The theme_showcase demo does exactly this with four themes.
A completely different visual (sharp corners, hard shadows, monospace, high contrast). Swap renderers:
use yororen_ui::brutalism_renderer;
brutalism_renderer::install(cx); // light/dark by appearance
brutalism_renderer::install_with_default_theme(cx); // force light
brutalism_renderer::install_with(cx, my_brutalism_theme); // custom brutalism JSONThe same headless::button("save", cx).on_click(...).render(cx) call works in all three renderers. The visual is what changes.
- Quick start — bootstrap that picks up these themes.
- 3-layer architecture — where the theme fits in the data flow.
Yororen UI v0.3.0 · repository · Apache-2.0 · This wiki documents Yororen UI v0.3.0.
This wiki documents Yororen UI v0.3.0 — the headless-core, swappable-renderer build.
{ "surface": { "base": "#FFFFFF", "canvas": "#F4F4F6", "raised": "#FBFBFD", "sunken": "#EFEFF2", "hover": "#E6E6EA", "popover": "#EFEFF2" }, "content": { "primary": "#141416", "secondary": "#3E3E45", "tertiary": "#6B6B73", "disabled": "#9A9AA2", "on_primary": "#FFFFFF", "on_status": "#0B0B0D" }, "border": { "default": "#D8D8DD", "muted": "#E3E3E8", "focus": "#2F63FF", "divider": "#E3E3E8" }, "action": { "neutral": { "bg": "#F1F1F3", "hover_bg": "#E6E6EA", "active_bg": "#DADADF", "fg": "#141416", "disabled_bg": "#E7E7EA", "disabled_fg": "#9A9AA2" }, "primary": { "bg": "#121214", "hover_bg": "#2A2A2E", "active_bg": "#404045", "fg": "#FFFFFF", "disabled_bg": "#2A2A2E", "disabled_fg": "#D0D0D6" }, "danger": { "bg": "#FFB4AE", "hover_bg": "#FFA099", "active_bg": "#FF8A82", "fg": "#0B0B0D", "disabled_bg": "#F0CBC7", "disabled_fg": "#9A9AA2" } }, "status": { "neutral": { "bg": "#E7E7EA", "fg": "#0B0B0D" }, "success": { "bg": "#B9F5C9", "fg": "#0B0B0D" }, "warning": { "bg": "#FFE1A6", "fg": "#0B0B0D" }, "danger": { "bg": "#FFB4AE", "fg": "#0B0B0D" }, "error": { "bg": "#FFB4AE", "fg": "#0B0B0D" }, "info": { "bg": "#B6D9FF", "fg": "#0B0B0D" } }, "shadow": { "elevation_1": "rgba(0,0,0,0.18)", "elevation_2": "rgba(0,0,0,0.30)" }, "tokens": { "sizes": { "control_h_md": 32, "control_h_lg": 36, /* … */ }, "radii": { "none": 0, "xs": 2, "sm": 4, "md": 6, "lg": 8, "xl": 12, "pill": 9999 }, "spacing": { "gap_1": 4, "gap_2": 8, "gap_3": 12, /* … */ }, "typography": { "font_size_md": 14, "font_size_lg": 16, /* … */ }, "motion": { "duration_fast": 150, "duration_normal": 200, /* … */ }, "control": { "button": { "min_height": 36, "horizontal_padding": 16, "radius": 6 }, "input": { "min_height": 32, "horizontal_padding": 12, "radius": 4 }, "modal": { "min_width": 320, "max_width": 520, "padding": 24, "border_radius": 12 }, "switch": { "track_w": 34, "track_h": 18, "knob_size": 14 } // one sub-object per component } } }