Intermediate 15 min

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.

src/view.rs
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.

src/view.rs
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.rs
Compiled example

An 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.

Open exact source
app_examples/docs-examples/src/examples/styling.rs
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)
    }
}