-
Notifications
You must be signed in to change notification settings - Fork 9
Guide Theming
v0.3: The default light/dark palette now ships in the
yororen-ui-theme-systemcrate (re-exported from the meta-crate asyororen_ui::theme_system). Components read colors and design tokens from the samecx.theme(); tokens let you resize the entire UI without touching component code.
- Yororen UI stores the active palette in a
gpui::GlobalcalledGlobalTheme. - Components access it via
ActiveTheme(cx.theme()). -
Theme::tokensexposesDesignTokens(sizes, radii, spacing, typography, motion, and per-control geometry).
use gpui::App;
use yororen_ui::theme_system;
fn init_theme(cx: &mut App) {
theme_system::install(cx, cx.window_appearance());
}theme_system::install builds a ThemeSet containing the default light
and dark palettes and registers it as the global theme. If your app has
its own asset bundle you can still ship your own colors — depend on
yororen-ui-core directly and call GlobalTheme::new_with_themes(...)
yourself (see below).
If you depend on yororen-ui-core directly (without the theme package)
and want the default palette without the helper, you can still call:
use gpui::App;
use yororen_ui::theme::GlobalTheme;
fn init_theme(cx: &mut App) {
cx.set_global(GlobalTheme::new(cx.window_appearance()));
}Yororen UI will choose Theme::default_light() or Theme::default_dark() based on the current
WindowAppearance.
Use ThemeSet + GlobalTheme::new_with_themes.
- Light theme is required.
- Dark theme is optional.
- If you do not provide a dark theme, Yororen UI will always use the light theme (even when the system/window appearance is dark).
use gpui::App;
use yororen_ui::theme::{GlobalTheme, Theme, ThemeSet};
fn init_theme(cx: &mut App) {
let light = Theme::default_light();
// Optional:
let dark = Theme::default_dark();
cx.set_global(GlobalTheme::new_with_themes(
cx.window_appearance(),
ThemeSet::new(light).dark(dark),
));
}use gpui::App;
use yororen_ui::theme::{GlobalTheme, Theme, ThemeSet};
fn init_theme(cx: &mut App) {
let light = Theme::default_light();
// No `.dark(...)`.
cx.set_global(GlobalTheme::new_with_themes(
cx.window_appearance(),
ThemeSet::new(light),
));
}Start from a default theme and override individual fields. The example below changes the focus ring to indigo and uses a tinted canvas background:
use gpui::{rgb, App};
use yororen_ui::theme::{GlobalTheme, Theme, ThemeSet};
fn init_theme(cx: &mut App) {
let mut light = Theme::default_light();
// Brand-colored focus ring
light.border.focus = rgb(0x6366F1).into();
// Slightly tinted canvas background
light.surface.canvas = rgb(0xF0F4FF).into();
// Custom primary action color (e.g. indigo button)
light.action.primary.bg = rgb(0x6366F1).into();
light.action.primary.hover_bg = rgb(0x4F46E5).into();
light.action.primary.active_bg = rgb(0x4338CA).into();
cx.set_global(GlobalTheme::new_with_themes(
cx.window_appearance(),
ThemeSet::new(light).dark(Theme::default_dark()),
));
}Tip: You can apply the same technique to build a fully custom dark theme by starting from
Theme::default_dark()and overriding the fields you need.
Yororen UI's Theme is not just a "primary color". It's a full color contract used across
components.
When you provide a custom theme, you must provide a complete Theme value for the light palette
(and optionally another complete Theme for the dark palette).
-
surface-
canvas: app/window background -
base: default surface background (cards/inputs) -
raised: elevated surface (popovers/title areas) -
sunken: recessed surface (tracks, wells) -
hover: hover surface fill
-
-
content-
primary: primary text -
secondary: secondary text -
tertiary: tertiary/hint text (placeholders) -
disabled: disabled text -
on_primary: text on top ofaction.primary.bg -
on_status: text on top ofstatus.*.bg
-
-
border-
default: default border -
muted: muted border -
focus: focus ring / focused border -
divider: divider line
-
-
action-
neutral,primary,danger(each is anActionVariant)
-
-
status-
success,warning,error,info(each is aStatusVariant)
-
-
shadowelevation_1elevation_2
-
text_direction: text direction (LtrorRtl). See Internationalization for RTL support.
Each action variant must provide:
bghover_bgactive_bgfgdisabled_bgdisabled_fg
Each status variant must provide:
bgfg
- Ensure adequate contrast for at least these pairs:
-
surface.basevscontent.primary -
action.neutral.bgvsaction.neutral.fg -
action.primary.bgvsaction.primary.fg -
status.*.bgvsstatus.*.fg
-
- Keep
content.on_primary/content.on_statusas "text on top of a solid color" values.
The theme::validate function returns a list of Issues for a given
Theme. The bundled light/dark palettes always pass with zero issues —
run it on your custom themes in CI to catch low-contrast text and
out-of-range token values before shipping.
use yororen_ui::theme::{validate, IssueKind};
let issues = validate(&theme);
for issue in &issues {
eprintln!("[theme] {}", issue.message);
}
assert!(issues.is_empty(), "theme validation failed");Currently validate checks:
- Contrast ratio for
surface.base/content.primary,action.{neutral,primary,danger}, allstatus.*(min 4.5:1), andborder.focusonsurface.base(min 3.0:1). - Switch geometry:
tokens.control.switch.knob_sizemust fit withintrack_h - padding*2. - Range checks on
motion.pulse_min_opacity/motion.pulse_max_opacity(must lie in[0.0, 1.0]).
IssueKind is a public enum (ContrastTooLow, TokenOutOfRange,
StatusTextInvisible, MalformedControlGeometry) so you can write
asserts in tests.
Every Theme carries a DesignTokens struct that components read
instead of hardcoded px(...) literals. Override fields in your custom
theme to reshape the entire UI without editing component code.
use gpui::px;
use yororen_ui::theme::{GlobalTheme, ThemeSet};
let mut dark = yororen_ui::theme_system::dark();
// Larger switch thumb for a denser, more "physical" feel
dark.tokens.control.switch.knob_size = px(18.);
// Slightly tighter spacing globally
dark.tokens.spacing.gap_2 = px(6.);
// Slow down modal motion
dark.tokens.motion.duration_modal_fade = std::time::Duration::from_millis(280);
cx.set_global(GlobalTheme::new_with_themes(
cx.window_appearance(),
ThemeSet::new(yororen_ui::theme_system::light()).dark(dark),
));Available namespaces (see yororen_ui::theme::tokens):
-
tokens.sizes— control heights (control_h_xxs…control_h_xl), icon sizes (icon_xxs…icon_xl), avatar sizes,status_dot. -
tokens.radii—none,xs(2),sm(4),md(6),lg(8),xl(12),pill(9999). -
tokens.spacing—gap_0…gap_7andinset_xs…inset_xl. -
tokens.typography—font_size_xxs…font_size_2xl, line heights,weight_*,family_default,family_mono. -
tokens.motion—duration_*(inDuration) andeasing_*(EasingFn=fn(f32) -> f32), pluspulse_*_opacity,slide_distance,bounce_distance. -
tokens.control— one sub-struct per component (button,input,switch,checkbox,radio,select,combo_box,slider,toast,modal,popover,dropdown,badge,tag,skeleton,progress,avatar,tooltip,disclosure,keybinding_input,split_button,search_input,number_input,file_path_input,icon_button,toggle_button,empty_state,list_item,tree_item,card,divider,form,notification,focus_ring).
Defaults are pinned to the v0.2 pixel values so existing apps see no visual change after the v0.3 migration.
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.