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
| Prop | Type | Default | Description |
|---|---|---|---|
primary_color | Option<String> | "blue" | Primary color name |
primary_shade | Option<u8> | 6 | Shade index (0-9) |
default_radius | Option<String> | "sm" | Border radius (xs, sm, md, lg, xl) |
font_family | Option<String> | System fonts | Primary font stack |
font_family_monospace | Option<String> | System mono | Monospace font stack |
dark_mode | bool | false | Dark 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-bodybecomes dark gray--rinch-color-textbecomes 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
| Color | Shade 0 (lightest) | Shade 6 (primary) | Notes |
|---|---|---|---|
dark | #C1C2C5 | #1A1B1E | Dark grays |
gray | #f8f9fa | #868e96 | gray-0 = default body background |
red | #fff5f5 | #fa5252 | |
pink | #fff0f6 | #e64980 | |
grape | #f8f0fc | #be4bdb | |
violet | #f3f0ff | #7950f2 | |
indigo | #edf2ff | #4c6ef5 | |
blue | #e7f5ff | #228be6 | Default primary |
cyan | #e3fafc | #15aabf | |
teal | #e6fcf5 | #12b886 | |
green | #ebfbee | #40c057 | |
lime | #f4fce3 | #82c91e | |
yellow | #fff9db | #fab005 | |
orange | #fff4e6 | #fd7e14 |
Gotcha:
--rinch-color-gray-0is#f8f9fa, which matches the default body background. If you use it as a card background, it’ll be invisible. Usegray-1or higher for visible backgrounds.