Style and theme
Choose a color scheme and accent, override widget tokens, and apply typed visual state.
Style with Rust values
Argui has no CSS parser or cascade. LayoutStyle, QuadStyle, TextStyle, and StylePatch make each property explicit and type checked.
Element::column(content)
.padding(Sides::length(20.0))
.gap(12.0)
.background(theme.card)
.border(Border::all(1.0, theme.border))
.radius(CornerRadii::all(12.0))Start from the default theme
default_theme(cx.environment()) builds matching light and dark WidgetTheme palettes. Resolve the active color scheme, then pass derived styles such as theme.button() or theme.input() to widgets.
let themes = default_theme(cx.environment());
let theme = themes.resolve(cx.environment().color_scheme);
Button::new("save", "Save", theme.button()).build()Override any token
ThemeOverrides is a sparse, typed map. Override only the colors or numeric effects your product owns; every other value continues to follow the default light or dark palette.
- Surfaces: background, card, popover, muted, and borders
- Content: foreground, muted foreground, and destructive colors
- Controls: primary, secondary, focus ring, input, switch, and scrollbar values
- Effects: overlay blur, shadows, and dialog backdrop
Reference files
These are the implementation and guide files used for this chapter.
docs/ui/styling.mdcrates/argui-theme/src/lib.rscrates/argui-theme/src/tokens.rscrates/argui-widgets/src/theme.rscrates/argui-widget-gallery/src/app/theme.rsAn interactive theme configurator compiled to WebAssembly
The Rust file below is imported verbatim by this page and compiled into the WebAssembly application running underneath it.
use std::sync::Arc;
use argui::{
core::{Color, ColorScheme},
paint::{Border, CornerRadii},
runtime::{Context, Render},
text::TextStyle,
theme::{ThemeOverrides, ThemeValue},
ui::{Element, EventHandler, FlexWrap, Sides, percent},
widgets::{Button, WidgetTheme, default_theme},
};
#[derive(Default)]
pub struct Example {
scheme: Option<ColorScheme>,
accent: usize,
custom_tokens: bool,
}
fn accent(index: usize, fallback: Color) -> Color {
match index {
1 => Color::from_srgb8(124, 58, 237),
2 => Color::from_srgb8(217, 119, 6),
_ => fallback,
}
}
fn custom_overrides(scheme: ColorScheme) -> Arc<ThemeOverrides> {
let mut tokens = ThemeOverrides::default();
let (background, card, muted, border) = match scheme {
ColorScheme::Light => (
Color::from_srgb8(245, 243, 255),
Color::from_srgb8(255, 255, 255),
Color::from_srgb8(237, 233, 254),
Color::from_srgb8(196, 181, 253),
),
ColorScheme::Dark => (
Color::from_srgb8(24, 20, 38),
Color::from_srgb8(34, 28, 53),
Color::from_srgb8(49, 40, 74),
Color::from_srgb8(109, 88, 164),
),
};
for (name, color) in [
("background", background),
("card", card),
("muted", muted),
("secondary", muted),
("border", border),
("input-border", border),
] {
tokens.set(name, ThemeValue::Color(color));
}
tokens.set("overlay-blur", ThemeValue::Number(10.0));
Arc::new(tokens)
}
fn choice(
key: &'static str,
label: &'static str,
selected: bool,
theme: &WidgetTheme,
handler: EventHandler,
) -> Element {
let style = if selected {
theme.button()
} else {
theme.outline_button()
};
Button::new(key, label, style).on_click(handler).build()
}
impl Render for Example {
fn render(&mut self, cx: &mut Context<Self>) -> Element {
let mut source = cx.environment().clone();
source.color_scheme = self.scheme.unwrap_or(source.color_scheme);
source.primary = accent(self.accent, source.primary);
if self.custom_tokens {
source.theme_overrides = Some(custom_overrides(source.color_scheme));
}
let themes = default_theme(&source);
let theme = themes.resolve(source.color_scheme);
let text = |value, size, weight, color| {
Element::text(value).text_style(TextStyle {
color,
font_size: size,
line_height: size * 1.35,
weight,
..TextStyle::default()
})
};
let schemes = Element::row([
choice(
"system",
"System",
self.scheme.is_none(),
theme,
cx.callback(|app| app.scheme = None),
),
choice(
"light",
"Light",
self.scheme == Some(ColorScheme::Light),
theme,
cx.callback(|app| app.scheme = Some(ColorScheme::Light)),
),
choice(
"dark",
"Dark",
self.scheme == Some(ColorScheme::Dark),
theme,
cx.callback(|app| app.scheme = Some(ColorScheme::Dark)),
),
])
.gap(8.0)
.flex_wrap(FlexWrap::Wrap);
let accents = Element::row([
choice(
"blue",
"Blue",
self.accent == 0,
theme,
cx.callback(|app| app.accent = 0),
),
choice(
"violet",
"Violet",
self.accent == 1,
theme,
cx.callback(|app| app.accent = 1),
),
choice(
"amber",
"Amber",
self.accent == 2,
theme,
cx.callback(|app| app.accent = 2),
),
choice(
"tokens",
if self.custom_tokens {
"Reset tokens"
} else {
"Override tokens"
},
self.custom_tokens,
theme,
cx.callback(|app| app.custom_tokens = !app.custom_tokens),
),
])
.gap(8.0)
.flex_wrap(FlexWrap::Wrap);
let preview = Element::column([
text("Live theme preview", 24.0, 720, theme.foreground),
text(
if self.custom_tokens {
"Background, card, muted, borders, inputs and blur are overridden."
} else {
"Every widget derives its states from the resolved default theme."
},
14.0,
450,
theme.muted_foreground,
),
Element::row([
Button::new("preview-primary", "Primary", theme.button()).build(),
Button::new("preview-secondary", "Secondary", theme.secondary_button()).build(),
Button::new("preview-delete", "Delete", theme.destructive_button()).build(),
])
.gap(8.0)
.flex_wrap(FlexWrap::Wrap),
])
.padding(Sides::length(18.0))
.gap(12.0)
.background(theme.card)
.border(Border::all(1.0, theme.border))
.radius(CornerRadii::all(if self.custom_tokens {
24.0
} else {
12.0
}));
Element::column([
text("Theme configurator", 28.0, 760, theme.foreground),
schemes,
accents,
preview,
])
.width(percent(1.0))
.height(percent(1.0))
.padding(Sides::length(20.0))
.gap(14.0)
.background(theme.background)
}
}
Starting this lesson’s WebAssembly module…