Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Theming

Rinch’s theme system is CSS-variable-based, Mantine-inspired, and designed to be extended rather than fought against. Enable it with the theme feature (auto-enabled by components):

rinch = { git = "...", features = ["desktop", "theme"] }

Quick Start

fn main() {
    let theme = ThemeProviderProps {
        primary_color: Some("cyan".into()),
        default_radius: Some("md".into()),
        dark_mode: false,
        ..Default::default()
    };
    run_with_theme("My App", 800, 600, app, theme);
}

That’s it. Every component picks up your colors, radius, and spacing through CSS variables. Toggle dark_mode: true and the whole UI flips. All semantic colors — body background, text, borders, placeholders — adjust automatically.

Use run() instead of run_with_theme() to get the default theme (blue primary, light mode, small radius).

ThemeProviderProps

PropTypeDefaultDescription
primary_colorOption<String>"blue"Primary color name
primary_shadeOption<u8>6Shade index (0-9)
default_radiusOption<String>"sm"Border radius (xs, sm, md, lg, xl)
font_familyOption<String>System fontsPrimary font stack
font_family_monospaceOption<String>System monoMonospace font stack
dark_modeboolfalseDark mode

CSS Variables

The theme generates CSS custom properties you can use anywhere in your styles.

One variable in this list — --rinch-window-top-inset — is not generated by the theme. It is published at runtime by whatever window chrome rinch is drawing, and is documented here because you consume it the same way as the rest.

Colors

14 named palettes, each with 10 shades (0 = lightest, 9 = darkest):

var(--rinch-color-blue-0)     /* Lightest blue */
var(--rinch-color-blue-5)     /* Mid blue */
var(--rinch-color-blue-9)     /* Darkest blue */

var(--rinch-primary-color)    /* Primary at your chosen shade */
var(--rinch-primary-color-0)  /* Lightest primary */
var(--rinch-primary-color-9)  /* Darkest primary */

Available palettes: dark, gray, red, pink, grape, violet, indigo, blue, cyan, teal, green, lime, yellow, orange.

Semantic Colors

These flip automatically in dark mode:

var(--rinch-color-body)        /* Background (#f8f9fa light, dark gray dark) */
var(--rinch-color-text)        /* Primary text */
var(--rinch-color-dimmed)      /* Secondary text */
var(--rinch-color-border)      /* Borders */
var(--rinch-color-placeholder) /* Input placeholders */

Spacing

var(--rinch-spacing-xs)   /* 10px */
var(--rinch-spacing-sm)   /* 12px */
var(--rinch-spacing-md)   /* 16px */
var(--rinch-spacing-lg)   /* 20px */
var(--rinch-spacing-xl)   /* 32px */

Border Radius

var(--rinch-radius-xs)       /* 2px */
var(--rinch-radius-sm)       /* 4px */
var(--rinch-radius-md)       /* 8px */
var(--rinch-radius-lg)       /* 16px */
var(--rinch-radius-xl)       /* 32px */
var(--rinch-radius-default)  /* Whatever you set in ThemeProviderProps */

Typography

var(--rinch-font-size-xs)   /* 12px */
var(--rinch-font-size-sm)   /* 14px */
var(--rinch-font-size-md)   /* 16px */
var(--rinch-font-size-lg)   /* 18px */
var(--rinch-font-size-xl)   /* 20px */

var(--rinch-line-height-xs) /* 1.4 */
var(--rinch-line-height-md) /* 1.55 */

var(--rinch-font-family)
var(--rinch-font-family-monospace)

/* Heading sizes (h1-h6) */
var(--rinch-h1-font-size)
var(--rinch-h1-line-height)
var(--rinch-h1-font-weight)

Shadows

var(--rinch-shadow-xs)
var(--rinch-shadow-sm)
var(--rinch-shadow-md)
var(--rinch-shadow-lg)
var(--rinch-shadow-xl)

Window Chrome Inset

var(--rinch-window-top-inset)  /* Height of the chrome rinch draws above your content */

Rinch sometimes draws window chrome itself: the in-app menu bar on Linux (28px) and the BorderlessWindow titlebar (36px). That chrome reserves space with in-document padding, so normal flow content clears it automatically.

A position: fixed element does not. Fixed elements resolve against the real viewport — that is what browsers do, and what rinch-web does on the real DOM — so a top: 0 overlay slides underneath the menu bar or titlebar.

The chrome therefore publishes its own height as --rinch-window-top-inset. It’s an ordinary inherited custom property, so any fixed descendant can opt in:

#![allow(unused)]
fn main() {
rsx! {
    div { style: "
        position: fixed;
        top: var(--rinch-window-top-inset, 0px);
        left: 0; right: 0; bottom: 0;
    ",
        "Full-height overlay that clears the menu bar"
    }
}
}

The 0px fallback matters: on a plain window there is no chrome and the variable is never set, so the overlay stays flush with the top.

Drawer, Modal, and the top-anchored Notification positions already do this. Deliberate full-window layers — the backdrops behind DropdownMenu and Select, which must catch clicks on the chrome too — do not, and shouldn’t.

Using Theme Variables in Your Styles

#![allow(unused)]
fn main() {
rsx! {
    div { style: "
        background: var(--rinch-color-body);
        padding: var(--rinch-spacing-md);
        border-radius: var(--rinch-radius-default);
        box-shadow: var(--rinch-shadow-sm);
    ",
        h1 { style: "color: var(--rinch-primary-color);", "Themed!" }
        p { style: "color: var(--rinch-color-dimmed);", "Using theme variables." }
    }
}
}

Or use CSS shorthand props, which resolve scale values automatically:

#![allow(unused)]
fn main() {
rsx! {
    div { p: "md", m: "sm",  // -> var(--rinch-spacing-md), var(--rinch-spacing-sm)
        "Shorthand is nicer"
    }
}
}

Extending the Theme

Scoped Overrides

CSS variables cascade. Override them on a container div and everything inside picks up the change:

#![allow(unused)]
fn main() {
rsx! {
    div { style: "
        --rinch-primary-color: #ff6b6b;
        --rinch-radius-default: 0px;
    ",
        // Red primary, sharp corners — only inside this div
        Button { "I'm red and sharp" }
        TextInput { placeholder: "Me too" }
    }
}
}

This is how you create themed sections, cards with different accent colors, or “danger zone” areas with red buttons — without changing the global theme.

Replacing the Theme Entirely

The theme is just CSS. If you hate it, replace it:

#![allow(unused)]
fn main() {
use rinch::theme::{Theme, ColorName};

let theme = Theme::builder()
    .primary_color(ColorName::Cyan)
    .dark_mode(true)
    .build();

// Generate the CSS string and do whatever you want with it
let css = rinch_theme::generate_theme_css(&theme);
}

Or ignore the theme system entirely and write your own CSS variables, your own classes, your own inline styles. Rinch components read from --rinch-* variables. If those variables exist, components use them. If they don’t, components fall back to hardcoded defaults. You’re never locked in.

Dark Mode

#![allow(unused)]
fn main() {
let theme = ThemeProviderProps {
    dark_mode: true,
    ..Default::default()
};
}

In dark mode, semantic colors flip:

  • --rinch-color-body becomes dark gray
  • --rinch-color-text becomes light
  • Component backgrounds, borders, and hover states all adjust

For dynamic dark mode toggling (e.g., a switch in your app), use dark_mode_fn on WASM or re-create the theme and call update_theme() on desktop.

Color Palette Reference

ColorShade 0 (lightest)Shade 6 (primary)Notes
dark#C1C2C5#1A1B1EDark grays
gray#f8f9fa#868e96gray-0 = default body background
red#fff5f5#fa5252
pink#fff0f6#e64980
grape#f8f0fc#be4bdb
violet#f3f0ff#7950f2
indigo#edf2ff#4c6ef5
blue#e7f5ff#228be6Default primary
cyan#e3fafc#15aabf
teal#e6fcf5#12b886
green#ebfbee#40c057
lime#f4fce3#82c91e
yellow#fff9db#fab005
orange#fff4e6#fd7e14

Gotcha: --rinch-color-gray-0 is #f8f9fa, which matches the default body background. If you use it as a card background, it’ll be invisible. Use gray-1 or higher for visible backgrounds.